optimize structure

This commit is contained in:
kangwei 2020-04-27 00:07:43 +08:00
parent c5f95c5316
commit b486a8419c
294 changed files with 8917 additions and 2992 deletions

View File

@ -1,8 +1,9 @@
import { Component, Fragment, PureComponent } from 'react';
import classNames from 'classnames';
import { computed, observer, TitleContent, Title } from '@ali/lowcode-globals';
import { computed, observer, Title } from '@ali/lowcode-editor-core';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { TitleContent } from '@ali/lowcode-types';
export class BorderHoveringInstance extends PureComponent<{
title: TitleContent;

View File

@ -9,19 +9,13 @@ import {
ComponentType,
} from 'react';
import classNames from 'classnames';
import {
observer,
computed,
createIcon,
EmbedTip,
isReactComponent,
ActionContentObject,
isActionContentObject,
} from '@ali/lowcode-globals';
import { observer, computed, Tip } from '@ali/lowcode-editor-core';
import { createIcon, isReactComponent } from '@ali/lowcode-utils';
import { ActionContentObject, isActionContentObject } from '@ali/lowcode-types';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import { OffsetObserver } from '../../../designer';
import { Node } from '../../../document';
import { OffsetObserver } from '../../designer';
import { Node } from '../../document';
@observer
export class BorderSelectingInstance extends Component<{
@ -94,9 +88,9 @@ class Toolbar extends Component<{ observed: OffsetObserver }> {
}
const { node } = observed;
const actions: ReactNodeArray = [];
node.componentMeta.availableActions.forEach(action => {
node.componentMeta.availableActions.forEach((action) => {
const { important, condition, content, name } = action;
if (node.isSlotRoot && (name === 'copy' || name === 'remove')) {
if (node.isSlot() && (name === 'copy' || name === 'remove')) {
// FIXME: need this?
return;
}
@ -130,7 +124,7 @@ function createAction(content: ReactNode | ComponentType<any> | ActionContentObj
}}
>
{icon && createIcon(icon)}
<EmbedTip>{title}</EmbedTip>
<Tip>{title}</Tip>
</div>
);
}
@ -167,7 +161,7 @@ export class BorderSelectingForNode extends Component<{ node: Node }> {
}
return (
<Fragment key={node.id}>
{instances.map(instance => {
{instances.map((instance) => {
const observed = designer.createOffsetObserver({
node,
instance,
@ -216,7 +210,7 @@ export class BorderSelecting extends Component {
return (
<Fragment>
{selecting.map(node => (
{selecting.map((node) => (
<BorderSelectingForNode key={node.id} node={node} />
))}
</Fragment>

View File

@ -1,5 +1,5 @@
import { Component } from 'react';
import { observer } from '@ali/lowcode-globals';
import { observer } from '@ali/lowcode-editor-core';
import { BorderHovering } from './border-hovering';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';

View File

@ -1,5 +1,5 @@
import { Component } from 'react';
import { computed, observer } from '@ali/lowcode-globals';
import { computed, observer } from '@ali/lowcode-editor-core';
import { SimulatorContext } from '../context';
import { BuiltinSimulatorHost } from '../host';
import {
@ -10,7 +10,7 @@ import {
isVertical
} from '../../designer';
import { ISimulatorHost, } from '../../simulator';
import {ParentalNode } from '../../document';
import { ParentalNode } from '../../document';
import './insertion.less';
interface InsertionData {

View File

@ -1,7 +1,7 @@
// NOTE: 仅用作类型标注,切勿作为实体使用
import { BuiltinSimulatorHost } from './host';
import { AssetLevel, AssetLevels, AssetList, isAssetBundle, isAssetItem, AssetType, assetItem } from '@ali/lowcode-globals';
import { isCSSUrl } from '@ali/lowcode-globals';
import { AssetLevel, AssetLevels, AssetList, isAssetBundle, isAssetItem, AssetType, assetItem } from '@ali/lowcode-utils';
import { isCSSUrl } from '@ali/lowcode-utils';
import { BuiltinSimulatorRenderer } from './renderer';
export function createSimulator(

View File

@ -1,5 +1,5 @@
import { Component } from 'react';
import { observer } from '@ali/lowcode-globals';
import { observer } from '@ali/lowcode-editor-core';
import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host';
import { DocumentModel } from '../document';
import { SimulatorContext } from './context';

View File

@ -1,10 +1,10 @@
import { obx, autorun, computed } from '@ali/lowcode-globals';
import { obx, autorun, computed, getPublicPath, hotkey } from '@ali/lowcode-editor-core';
import { ISimulatorHost, Component, NodeInstance, ComponentInstance } from '../simulator';
import Viewport from './viewport';
import { createSimulator } from './create-simulator';
import { Node, ParentalNode, DocumentModel, isNode, contains, isRootNode } from '../document';
import ResourceConsumer from './resource-consumer';
import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, getPublicPath } from '@ali/lowcode-globals';
import { AssetLevel, Asset, AssetList, assetBundle, assetItem, AssetType, isElement } from '@ali/lowcode-utils';
import {
DragObjectType,
isShaken,
@ -21,9 +21,8 @@ import {
Rect,
CanvasPoint,
} from '../designer';
import { parseProps, parseMetadata } from './utils/parse-metadata';
import { isElement, hotkey } from '@ali/lowcode-globals';
import { ComponentMetadata } from '@ali/lowcode-globals';
import { parseMetadata } from './utils/parse-metadata';
import { ComponentMetadata } from '@ali/lowcode-types';
import { BuiltinSimulatorRenderer } from './renderer';
import clipboard from '../designer/clipboard';
@ -967,7 +966,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp
return this.document.checkDropTarget(container, dragObject as any);
}
const meta = container.componentMeta;
const meta = (container as Node).componentMeta;
// FIXME: get containerInstance for accept logic use
const acceptable: boolean = this.isAcceptable(container);

View File

@ -1,4 +1,4 @@
import { autorun, obx } from '@ali/lowcode-globals';
import { autorun, obx } from '@ali/lowcode-editor-core';
import { BuiltinSimulatorHost } from './host';
import { EventEmitter } from 'events';
import { BuiltinSimulatorRenderer, isSimulatorRenderer } from './renderer';

View File

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import { isValidElement } from 'react';
import { isElement } from '@ali/lowcode-globals';
import { PropConfig } from '@ali/lowcode-globals';
import { isElement } from '@ali/lowcode-utils';
import { PropConfig } from '@ali/lowcode-types';
export const primitiveTypes = [
'string',

View File

@ -1,4 +1,4 @@
import { obx, computed } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { Point, ScrollTarget } from '../designer';
import { AutoFit, IViewport } from '../simulator';

View File

@ -6,11 +6,9 @@ import {
ComponentAction,
TitleContent,
TransformedComponentMetadata,
getRegisteredMetadataTransducers,
registerMetadataTransducer,
computed,
NestingFilter,
} from '@ali/lowcode-globals';
} from '@ali/lowcode-types';
import { computed } from '@ali/lowcode-editor-core';
import { Node, ParentalNode } from './document';
import { Designer } from './designer';
import { intl } from './locale';
@ -130,11 +128,14 @@ export class ComponentMeta {
const title = this._transformedMetadata.title;
if (title) {
this._title = typeof title === 'string' ? {
type: 'i18n',
'en-US': this.componentName,
'zh-CN': title,
} : title;
this._title =
typeof title === 'string'
? {
type: 'i18n',
'en-US': this.componentName,
'zh-CN': title,
}
: title;
}
const { configure = {} } = this._transformedMetadata;
@ -300,7 +301,7 @@ const builtinComponentActions: ComponentAction[] = [
];
export function removeBuiltinComponentAction(name: string) {
const i = builtinComponentActions.findIndex(action => action.name === name);
const i = builtinComponentActions.findIndex((action) => action.name === name);
if (i > -1) {
builtinComponentActions.splice(i, 1);
}
@ -308,3 +309,33 @@ export function removeBuiltinComponentAction(name: string) {
export function addBuiltinComponentAction(action: ComponentAction) {
builtinComponentActions.push(action);
}
export interface MetadataTransducer {
(prev: TransformedComponentMetadata): TransformedComponentMetadata;
/**
* 0 - 9 system
* 10 - 99 builtin-plugin
* 100 - app & plugin
*/
level?: number;
/**
* use to replace TODO
*/
id?: string;
}
const metadataTransducers: MetadataTransducer[] = [];
export function registerMetadataTransducer(transducer: MetadataTransducer, level: number = 100, id?: string) {
transducer.level = level;
transducer.id = id;
const i = metadataTransducers.findIndex((item) => item.level != null && item.level > level);
if (i < 0) {
metadataTransducers.push(transducer);
} else {
metadataTransducers.splice(i, 0, transducer);
}
}
export function getRegisteredMetadataTransducers(): MetadataTransducer[] {
return metadataTransducers;
}

View File

@ -1,6 +1,7 @@
import { hotkey, isFormEvent } from '@ali/lowcode-globals';
import { hotkey } from '@ali/lowcode-editor-core';
import { isFormEvent } from '@ali/lowcode-utils';
import { focusing } from './focusing';
import { insertChildren } from '../document';
import { insertChildren, TransformStage } from '../document';
import clipboard from './clipboard';
// hotkey binding
@ -52,7 +53,7 @@ hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
if (!selected || selected.length < 1) return;
const componentsMap = {};
const componentsTree = selected.map((item) => item.export(false));
const componentsTree = selected.map((item) => item.export(TransformStage.Save));
const data = { type: 'nodeSchema', componentsMap, componentsTree };

View File

@ -1,6 +1,5 @@
import { Component } from 'react';
import classNames from 'classnames';
import { TipContainer } from '@ali/lowcode-globals';
import BuiltinDragGhostComponent from './drag-ghost';
import { Designer, DesignerProps } from './designer';
import { ProjectView } from '../project';

View File

@ -1,16 +1,14 @@
import { ComponentType } from 'react';
import { obx, computed, autorun } from '@ali/lowcode-editor-core';
import {
ProjectSchema,
ComponentMetadata,
ComponentAction,
NpmInfo,
obx,
computed,
autorun,
IEditor,
CompositeObject,
PropsList,
} from '@ali/lowcode-globals';
} from '@ali/lowcode-types';
import { Project } from '../project';
import { Node, DocumentModel, insertChildren, isRootNode, ParentalNode, TransformStage } from '../document';
import { ComponentMeta } from '../component-meta';

View File

@ -1,5 +1,5 @@
import { Component } from 'react';
import { observer, obx, Title } from '@ali/lowcode-globals';
import { observer, obx, Title } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DragObject, isDragNodeObject, isDragNodeDataObject } from '../dragon';
import './ghost.less';

View File

@ -1,11 +1,11 @@
import { EventEmitter } from 'events';
import { NodeSchema, obx } from '@ali/lowcode-globals';
import { obx } from '@ali/lowcode-editor-core';
import { NodeSchema } from '@ali/lowcode-types';
import { setNativeSelection, cursor } from '@ali/lowcode-utils';
import { DropLocation } from './location';
import { Node, DocumentModel } from '../document';
import { ISimulatorHost, isSimulatorHost } from '../simulator';
import { Designer } from './designer';
import { setNativeSelection } from '@ali/lowcode-globals';
import { cursor } from '@ali/lowcode-globals';
export interface LocateEvent {
readonly type: 'LocateEvent';

View File

@ -1,4 +1,4 @@
import { obx } from '@ali/lowcode-globals';
import { obx } from '@ali/lowcode-editor-core';
import { Node, DocumentModel } from '../document';
export class Hovering {

View File

@ -1,5 +1,5 @@
import { obx, computed } from '@ali/lowcode-globals';
import { uniqueId } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { uniqueId } from '@ali/lowcode-utils';
import { INodeSelector, IViewport } from '../simulator';
import { isRootNode, Node } from '../document';

View File

@ -1,4 +1,4 @@
import { isElement } from '@ali/lowcode-globals';
import { isElement } from '@ali/lowcode-utils';
export class ScrollTarget {
get left() {

View File

@ -1,4 +1,4 @@
import { SettingTarget } from '@ali/lowcode-globals';
import { SettingTarget } from '@ali/lowcode-types';
import { ComponentMeta } from '../../component-meta';
import { Designer } from '../designer';
import { Node } from '../../document';

View File

@ -1,7 +1,8 @@
import { TitleContent, computed, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView, obx } from '@ali/lowcode-globals';
import { TitleContent, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView } from '@ali/lowcode-types';
import { Transducer } from './utils';
import { SettingPropEntry } from './setting-prop-entry';
import { SettingEntry } from './setting-entry';
import { computed, obx } from '@ali/lowcode-editor-core';
export class SettingField extends SettingPropEntry implements SettingEntry {
readonly isSettingField = true;

View File

@ -1,4 +1,6 @@
import { obx, uniqueId, computed, IEditor } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { IEditor } from '@ali/lowcode-types';
import { uniqueId } from '@ali/lowcode-utils';
import { SettingEntry } from './setting-entry';
import { Node } from '../../document';
import { ComponentMeta } from '../../component-meta';

View File

@ -1,5 +1,6 @@
import { EventEmitter } from 'events';
import { CustomView, computed, isCustomView, IEditor } from '@ali/lowcode-globals';
import { CustomView, isCustomView, IEditor } from '@ali/lowcode-types';
import { computed } from '@ali/lowcode-editor-core';
import { SettingEntry } from './setting-entry';
import { SettingField } from './setting-field';
import { SettingPropEntry } from './setting-prop-entry';

View File

@ -1,17 +1,5 @@
import {
NodeData,
isJSExpression,
isDOMText,
NodeSchema,
computed,
obx,
autorun,
isNodeSchema,
uniqueId,
PageSchema,
ComponentSchema,
RootSchema,
} from '@ali/lowcode-globals';
import { computed, obx } from '@ali/lowcode-editor-core';
import { NodeData, isJSExpression, isDOMText, NodeSchema, isNodeSchema, RootSchema } from '@ali/lowcode-types';
import { Project } from '../project';
import { ISimulatorHost } from '../simulator';
import { ComponentMeta } from '../component-meta';
@ -20,6 +8,7 @@ import { Node, insertChildren, insertChild, isNode, RootNode, ParentalNode } fro
import { Selection } from './selection';
import { History } from './history';
import { TransformStage } from './node';
import { uniqueId } from '@ali/lowcode-utils';
export type GetDataType<T, NodeType> = T extends undefined
? NodeType extends {
@ -90,11 +79,13 @@ export class DocumentModel {
this._blank = true;
}
this.rootNode = this.createNode<RootNode>(schema || {
componentName: 'Page',
id: 'root',
fileName: ''
});
this.rootNode = this.createNode<RootNode>(
schema || {
componentName: 'Page',
id: 'root',
fileName: '',
},
);
this.history = new History(
() => this.schema,

View File

@ -1,6 +1,6 @@
import { Component } from 'react';
import classNames from 'classnames';
import { observer } from '@ali/lowcode-globals';
import { observer } from '@ali/lowcode-editor-core';
import { DocumentModel } from './document-model';
import { BuiltinSimulatorHostView } from '../builtin-simulator';

View File

@ -1,5 +1,6 @@
import { EventEmitter } from 'events';
import { NodeSchema, autorun, Reaction, untracked } from '@ali/lowcode-globals';
import { autorun, Reaction, untracked } from '@ali/lowcode-editor-core';
import { NodeSchema } from '@ali/lowcode-types';
// TODO: cache to localStorage

View File

@ -1,5 +1,6 @@
import { obx, computed, TitleContent } from '@ali/lowcode-globals';
import { uniqueId } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { uniqueId } from '@ali/lowcode-utils';
import { TitleContent } from '@ali/lowcode-types';
import { Node } from './node';
import { intl } from '../../locale';

View File

@ -1,6 +1,7 @@
import { NodeData, isNodeSchema, obx, computed } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { Node, ParentalNode } from './node';
import { TransformStage } from './transform-stage';
import { NodeData, isNodeSchema } from '@ali/lowcode-types';
export class NodeChildren {
@obx.val private children: Node[];

View File

@ -1,3 +1,4 @@
import { obx, computed } from '@ali/lowcode-editor-core';
import {
isDOMText,
isJSExpression,
@ -6,12 +7,10 @@ import {
PropsList,
NodeData,
TitleContent,
obx,
computed,
SlotSchema,
PageSchema,
ComponentSchema,
} from '@ali/lowcode-globals';
} from '@ali/lowcode-types';
import { Props, EXTRA_KEY_PREFIX } from './props/props';
import { DocumentModel } from '../document-model';
import { NodeChildren } from './node-children';
@ -20,8 +19,6 @@ import { ComponentMeta } from '../../component-meta';
import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group';
import { TransformStage } from './transform-stage';
/**
*
*
@ -175,7 +172,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
}
isRoot(): this is RootNode {
return this.document.rootNode == this as any;
return this.document.rootNode == (this as any);
}
isPage(): this is PageNode {
@ -298,7 +295,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
@computed get slots() {
// TODO: optimize recore/obx, array maked every time, donot as changed
const slots: Node[] = [];
this.props.forEach(item => {
this.props.forEach((item) => {
if (item.type === 'slot') {
slots.push(item.slotNode!);
}
@ -563,7 +560,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
this.children?.insert(node, ref ? ref.index : null);
}
insertAfter(node: Node, ref?: Node) {
this.children?.insert(node, ref ? (ref.index + 1) : null);
this.children?.insert(node, ref ? ref.index + 1 : null);
}
getParent() {
return this.parent;
@ -594,9 +591,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
/**
* @deprecated
*/
setStatus() {
}
setStatus() {}
/**
* @deprecated
*/

View File

@ -1,4 +1,4 @@
import { obx, autorun, untracked, computed } from '@ali/lowcode-globals';
import { obx, autorun, untracked, computed } from '@ali/lowcode-editor-core';
import { Prop, IPropParent, UNSET } from './prop';
import { Props } from './props';

View File

@ -1,16 +1,6 @@
import {
CompositeValue,
isJSExpression,
isJSSlot,
untracked,
computed,
obx,
JSSlot,
SlotSchema
} from '@ali/lowcode-globals';
import { uniqueId } from '@ali/lowcode-globals';
import { isPlainObject } from '@ali/lowcode-globals';
import { hasOwnProperty } from '@ali/lowcode-globals';
import { untracked, computed, obx } from '@ali/lowcode-editor-core';
import { CompositeValue, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@ali/lowcode-types';
import { uniqueId, isPlainObject, hasOwnProperty } from '@ali/lowcode-utils';
import { PropStash } from './prop-stash';
import { valueToSource } from './value-to-source';
import { Props } from './props';

View File

@ -1,10 +1,12 @@
import { PropsMap, PropsList, CompositeValue, computed, obx, uniqueId } from '@ali/lowcode-globals';
import { computed, obx } from '@ali/lowcode-editor-core';
import { PropsMap, PropsList, CompositeValue } from '@ali/lowcode-types';
import { uniqueId } from '@ali/lowcode-utils';
import { PropStash } from './prop-stash';
import { Prop, IPropParent, UNSET } from './prop';
import { Node } from '../node';
import { TransformStage } from '../transform-stage';
export const EXTRA_KEY_PREFIX = '__';
export const EXTRA_KEY_PREFIX = '___';
export class Props implements IPropParent {
readonly id = uniqueId('props');

View File

@ -1,5 +1,5 @@
import { EventEmitter } from 'events';
import { obx } from '@ali/lowcode-globals';
import { obx } from '@ali/lowcode-editor-core';
import { Node, comparePosition, PositionNO } from './node/node';
import { DocumentModel } from './document-model';

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from "@ali/lowcode-globals";
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconClone(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from "@ali/lowcode-globals";
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconComponent(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from "@ali/lowcode-globals";
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconContainer(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from "@ali/lowcode-globals";
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconHidden(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from "@ali/lowcode-globals";
import { SVGIcon, IconProps } from "@ali/lowcode-utils";
export function IconPage(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from '@ali/lowcode-globals';
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconRemove(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { SVGIcon, IconProps } from '@ali/lowcode-globals';
import { SVGIcon, IconProps } from '@ali/lowcode-utils';
export function IconSetting(props: IconProps) {
return (

View File

@ -1,4 +1,4 @@
import { createIntl } from '@ali/lowcode-globals';
import { createIntl } from '@ali/lowcode-editor-core';
import en_US from './en-US.json';
import zh_CN from './zh-CN.json';

View File

@ -1,5 +1,5 @@
import { Component } from 'react';
import { observer } from '@ali/lowcode-globals';
import { observer } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DocumentView } from '../document';
import { intl } from '../locale';

View File

@ -1,7 +1,8 @@
import { EventEmitter } from 'events';
import { ProjectSchema, RootSchema, obx, computed } from '@ali/lowcode-globals';
import { obx, computed } from '@ali/lowcode-editor-core';
import { Designer } from '../designer';
import { DocumentModel, isDocumentModel } from '../document';
import { ProjectSchema, RootSchema } from '@ali/lowcode-types';
export class Project {
private emitter = new EventEmitter();

View File

@ -1,5 +1,5 @@
import { Component as ReactComponent, ComponentType } from 'react';
import { ComponentMetadata } from '@ali/lowcode-globals';
import { ComponentMetadata } from '@ali/lowcode-types';
import { ISensor, Point, ScrollTarget, IScrollable } from './designer';
import { Node } from './document';

View File

@ -1,47 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
<a name="0.8.6"></a>
## [0.8.6](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.5...@ali/lowcode-editor-core@0.8.6) (2020-04-16)
**Note:** Version bump only for package @ali/lowcode-editor-core
<a name="0.8.5"></a>
## [0.8.5](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.4...@ali/lowcode-editor-core@0.8.5) (2020-04-15)
### Bug Fixes
* editor ([ccd9162](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/commit/ccd9162))
<a name="0.8.4"></a>
## [0.8.4](https://gitlab.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/compare/@ali/lowcode-editor-core@0.8.3...@ali/lowcode-editor-core@0.8.4) (2020-03-30)
**Note:** Version bump only for package @ali/lowcode-editor-core
<a name="0.8.3"></a>
## 0.8.3 (2020-03-30)
**Note:** Version bump only for package @ali/lowcode-editor-core
<a name="0.8.2"></a>
## 0.8.2 (2020-03-30)
**Note:** Version bump only for package @ali/lowcode-editor-core

View File

@ -1,11 +1,3 @@
# demo component
shared globals
t-s-demo
intro component
## API
| 参数名 | 说明 | 必填 | 类型 | 默认值 | 备注 |
| ------ | ---- | ---- | ---- | ------ | ---- |
| | | | | | |
发 CDN

View File

@ -1,5 +1,9 @@
{
"plugins": [
"build-plugin-component"
"build-plugin-component",
"build-plugin-fusion",
["build-plugin-moment-locales", {
"locales": ["zh-cn"]
}]
]
}

View File

@ -1,45 +1,56 @@
{
"name": "@ali/lowcode-editor-core",
"version": "0.8.6",
"description": "alibaba lowcode editor core",
"name": "@ali/lowcode-globals",
"version": "0.9.3",
"description": "Globals api for Ali lowCode engine",
"license": "MIT",
"main": "lib/index.js",
"module": "es/index.js",
"stylePath": "style.js",
"files": [
"lib",
"es"
],
"scripts": {
"build": "build-scripts build --skip-demo",
"cloud-build": "build-scripts build --skip-demo --config cloud-build.json",
"test": "ava",
"test:snapshot": "ava --update-snapshots"
},
"keywords": [
"lowcode",
"editor"
],
"author": "xiayang.xy",
"ava": {
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register"
],
"snapshotDir": "test/fixtures/__snapshots__"
},
"dependencies": {
"@ali/lowcode-globals": "^0.9.3",
"@alifd/next": "1.x",
"@alifd/next": "^1.19.16",
"@recore/obx": "^1.0.8",
"@recore/obx-react": "^1.0.7",
"classnames": "^2.2.6",
"power-di": "^2.2.4",
"debug": "^4.1.1",
"events": "^3.1.0",
"intl-messageformat": "^8.3.1",
"lodash": "^4.17.15",
"prop-types": "^15.5.8",
"react": "^16.8.0",
"store": "^2.0.12",
"whatwg-fetch": "^3.0.0"
"react": "^16",
"react-dom": "^16.7.0"
},
"devDependencies": {
"@alib/build-scripts": "^0.1.3",
"@alib/build-scripts": "^0.1.18",
"@types/classnames": "^2.2.7",
"@types/node": "^13.7.1",
"@types/react": "^16",
"@types/react-dom": "^16",
"@types/lodash": "^4.14.149",
"@types/react": "^16.9.13",
"@types/react-dom": "^16.9.4",
"@types/store": "^2.0.2",
"build-plugin-component": "^0.2.10"
"build-plugin-component": "^0.2.11",
"build-plugin-fusion": "^0.1.0",
"build-plugin-moment-locales": "^0.1.0"
},
"license": "MIT",
"publishConfig": {
"registry": "https://registry.npm.alibaba-inc.com"
}

View File

@ -1,75 +0,0 @@
import { PluginConfig, PluginStatus, PluginClass, HOCPlugin } from './definitions';
import Editor from './index';
import { clone, deepEqual } from './utils';
export default class AreaManager {
private pluginStatus: PluginStatus;
private config: PluginConfig[];
constructor(private editor: Editor, private name: string) {
this.config = (editor && editor.config && editor.config.plugins && editor.config.plugins[name]) || [];
this.pluginStatus = clone(editor.pluginStatus);
}
setVisible(flag: boolean) {
}
isEnable() {
}
isVisible() {
}
isEmpty() {
}
isPluginStatusUpdate(pluginType?: string, notUpdateStatus?: boolean): boolean {
const { pluginStatus } = this.editor;
const list = pluginType ? this.config.filter((item): boolean => item.type === pluginType) : this.config;
const isUpdate = list.some(
(item): boolean => !deepEqual(pluginStatus[item.pluginKey], this.pluginStatus[item.pluginKey]),
);
if (!notUpdateStatus) {
this.pluginStatus = clone(pluginStatus);
}
return isUpdate;
}
getVisiblePluginList(pluginType?: string): PluginConfig[] {
const res = this.config.filter((item): boolean => {
return !!(!this.pluginStatus[item.pluginKey] || this.pluginStatus[item.pluginKey].visible);
});
return pluginType ? res.filter((item): boolean => item.type === pluginType) : res;
}
getPlugin(pluginKey: string): HOCPlugin | void {
if (pluginKey) {
return this.editor && this.editor.plugins && this.editor.plugins[pluginKey];
}
}
getPluginConfig(pluginKey?: string): PluginConfig[] | PluginConfig | undefined {
if (pluginKey) {
return this.config.find(item => item.pluginKey === pluginKey);
}
return this.config;
}
getPluginClass(pluginKey: string): PluginClass | void {
if (pluginKey) {
return this.editor && this.editor.components && this.editor.components[pluginKey];
}
}
getPluginStatus(pluginKey: string): PluginStatus | void {
if (pluginKey) {
return this.editor && this.editor.pluginStatus && this.editor.pluginStatus[pluginKey];
}
}
}

View File

@ -1,4 +0,0 @@
import { createContext } from 'react';
const context = createContext({});
export default context;

View File

@ -1,4 +1,3 @@
export * from './setter';
export * from './transducer';
export * from './ioc-context';
export * from './editor';
export * from './tip';

View File

@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { CustomView, isCustomView } from '../types/setter-config';
import { createContent } from '../utils/create-content';
import { TitleContent } from '../types';
import { CustomView, isCustomView, TitleContent } from '@ali/lowcode-types';
import { createContent } from '@ali/lowcode-utils';
export type RegisteredSetter = {
component: CustomView;

View File

@ -1,31 +1,10 @@
import { EventEmitter } from 'events';
import store from 'store';
import { IocContext, RegisterOptions } from '@ali/lowcode-globals';
import {
EditorConfig,
HooksConfig,
LocaleType,
PluginStatusSet,
Utils,
PluginClassSet,
PluginSet,
} from './definitions';
import pluginFactory from './pluginFactory';
import * as editorUtils from './utils';
const { registShortCuts, transformToPromise, unRegistShortCuts } = editorUtils;
let instance: Editor;
import { IEditor, EditorConfig, PluginClassSet } from '@ali/lowcode-types';
import { IocContext, RegisterOptions } from './di';
import { globalLocale } from './intl';
EventEmitter.defaultMaxListeners = 100;
export interface HooksFuncs {
[idx: number]: (msg: string, handler: (...args: []) => void) => void;
}
export type KeyType = Function | symbol | string;
export type KeyType = Function | Symbol | string;
export type ClassType = Function | (new (...args: any[]) => any);
export interface GetOptions {
forceNew?: boolean;
@ -41,26 +20,7 @@ export type GetReturnType<T, ClsType> = T extends undefined
const NOT_FOUND = Symbol.for('not_found');
export default class Editor extends EventEmitter {
static getInstance = (config: EditorConfig, components: PluginClassSet, utils?: Utils): Editor => {
if (!instance) {
instance = new Editor(config, components, utils);
}
return instance;
};
private _components?: PluginClassSet;
get components(): PluginClassSet {
if (!this._components) {
this._components = {};
Object.keys(this.componentsMap).forEach((key) => {
(this._components as any)[key] = pluginFactory(this.componentsMap[key]);
});
}
return this._components;
}
readonly utils: Utils;
export class Editor extends EventEmitter implements IEditor {
/**
* Ioc Container
*/
@ -68,18 +28,12 @@ export default class Editor extends EventEmitter {
notFoundHandler: (type: KeyType) => NOT_FOUND,
});
pluginStatus?: PluginStatusSet;
get locale() {
return globalLocale.getLocale();
}
plugins?: PluginSet;
locale?: LocaleType;
hooksFuncs?: HooksFuncs;
constructor(readonly config: EditorConfig = {}, readonly componentsMap: PluginClassSet = {}, utils?: Utils) {
constructor(readonly config: EditorConfig = {}, readonly components: PluginClassSet = {}) {
super();
this.utils = { ...editorUtils, ...utils } as any;
instance = this;
}
get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType<T, KeyOrType> | undefined {
@ -135,18 +89,15 @@ export default class Editor extends EventEmitter {
}
async init(): Promise<any> {
const { hooks, shortCuts = [], lifeCycles } = this.config || {};
this.locale = store.get('lowcode-editor-locale') || 'zh-CN';
this.pluginStatus = this.initPluginStatus();
this.initHooks(hooks || []);
const { shortCuts = [], lifeCycles } = this.config || {};
this.emit('editor.beforeInit');
const init = (lifeCycles && lifeCycles.init) || ((): void => {});
// 用户可以通过设置extensions.init自定义初始化流程
try {
await transformToPromise(init(this));
// await transformToPromise(init(this));
// 注册快捷键
registShortCuts(shortCuts, this);
// registShortCuts(shortCuts, this);
this.emit('editor.afterInit');
return true;
} catch (err) {
@ -156,9 +107,8 @@ export default class Editor extends EventEmitter {
destroy(): void {
try {
const { hooks = [], shortCuts = [], lifeCycles = {} } = this.config;
unRegistShortCuts(shortCuts);
this.destroyHooks(hooks);
const { shortCuts = [], lifeCycles = {} } = this.config;
// unRegistShortCuts(shortCuts);
if (lifeCycles.destroy) {
lifeCycles.destroy(this);
}
@ -167,33 +117,6 @@ export default class Editor extends EventEmitter {
}
}
batchOn(events: string[], lisenter: (...args: any[]) => void): void {
if (!Array.isArray(events)) {
return;
}
events.forEach((event): void => {
this.on(event, lisenter);
});
}
batchOnce(events: string[], lisenter: (...args: any[]) => void): void {
if (!Array.isArray(events)) {
return;
}
events.forEach((event): void => {
this.once(event, lisenter);
});
}
batchOff(events: string[], lisenter: (...args: any[]) => void): void {
if (!Array.isArray(events)) {
return;
}
events.forEach((event): void => {
this.off(event, lisenter);
});
}
private waits = new Map<
KeyType,
Array<{
@ -245,50 +168,4 @@ export default class Editor extends EventEmitter {
this.waits.delete(key);
}
}
// 销毁hooks中的消息监听
private destroyHooks(hooks: HooksConfig = []): void {
hooks.forEach((item, idx): void => {
if (typeof this.hooksFuncs?.[idx] === 'function') {
this.off(item.message, this.hooksFuncs[idx]);
}
});
delete this.hooksFuncs;
}
// 初始化hooks中的消息监听
private initHooks(hooks: HooksConfig = []): void {
this.hooksFuncs = hooks.map((item): ((...arg: any[]) => void) => {
const func = (...args: any[]): void => {
item.handler(this, ...args);
};
this[item.type](item.message, func);
return func;
});
}
private initPluginStatus(): PluginStatusSet {
const { plugins = {} } = this.config;
const pluginAreas = Object.keys(plugins);
const res: PluginStatusSet = {};
pluginAreas.forEach((area): void => {
(plugins[area] || []).forEach((plugin): void => {
if (plugin.type === 'Divider') {
return;
}
const { visible, disabled, marked } = plugin.props || {};
res[plugin.pluginKey] = {
visible: typeof visible === 'boolean' ? visible : true,
disabled: typeof disabled === 'boolean' ? disabled : false,
marked: typeof marked === 'boolean' ? marked : false,
};
const pluginClass = this.components[plugin.pluginKey];
// 判断如果编辑器插件有init静态方法则在此执行init方法
if (pluginClass && pluginClass.init) {
pluginClass.init(this);
}
});
});
return res;
}
}

View File

@ -1,11 +1,6 @@
import Editor from './editor';
import * as utils from './utils';
export { default as PluginFactory } from './pluginFactory';
export { default as EditorContext } from './context';
export { default as AreaManager } from './areaManager';
export default Editor;
export { Editor, utils };
export * from './intl';
export * from './editor';
export * from './utils';
export * from './di';
export * from './hotkey';
export * from './widgets';

View File

@ -1,4 +1,5 @@
import { EventEmitter } from 'events';
import { obx } from '../utils/obx';
const languageMap: { [key: string]: string } = {
en: 'en-US',
zh: 'zh-CN',
@ -28,7 +29,7 @@ const languageMap: { [key: string]: string } = {
const LowcodeConfigKey = 'ali-lowcode-config';
class AliGlobalLocale {
private locale: string = '';
@obx.ref private locale: string = '';
private emitter = new EventEmitter();
constructor() {

View File

@ -1,19 +1,9 @@
import { ReactNode, Component, createElement } from 'react';
import { IntlMessageFormat } from 'intl-messageformat';
import { globalLocale } from './ali-global-locale';
import { PureComponent, ReactNode } from 'react';
import { isI18nData } from '../types';
import { isI18nData } from '@ali/lowcode-types';
import { observer, computed } from '../utils';
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;
});
}
function generateTryLocales(locale: string) {
const tries = [locale, locale.replace('-', '_')];
if (locale === 'zh-TW' || locale === 'en-US') {
@ -30,44 +20,40 @@ function generateTryLocales(locale: string) {
return tries;
}
export function localeFormat(data: any, params?: object): string {
function injectVars(msg: string, params: any, locale: string): string {
if (!msg || !params) {
return msg;
}
const formater = new IntlMessageFormat(msg, locale);
return formater.format(params as any) as string;
/*
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 function intl(data: any, params?: object): string {
if (!isI18nData(data)) {
return data;
}
const locale = globalLocale.getLocale();
const tries = generateTryLocales(locale);
let tpl: string | undefined;
let msg: string | undefined;
for (const lan of tries) {
tpl = data[lan];
if (tpl != null) {
msg = data[lan];
if (msg != null) {
break;
}
}
if (tpl == null) {
if (msg == null) {
return `##intl@${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)) {
if (data.intl) {
return data.intl;
}
return <Intl data={data} params={params} />;
}
return data;
return injectVars(msg, params, locale);
}
export function shallowIntl(data: any): any {
@ -76,38 +62,53 @@ export function shallowIntl(data: any): any {
}
const maps: any = {};
Object.keys(data).forEach(key => {
maps[key] = localeFormat(data[key]);
maps[key] = intl(data[key]);
});
return maps;
}
export function intlNode(data: any, params?: object): ReactNode {
if (isI18nData(data)) {
if (data.intlNode) {
return data.intlNode;
}
return createElement(IntlElement, { data, params });
}
return data;
}
@observer
class IntlElement extends Component<{ data: any; params?: object }> {
render() {
const { data, params } = this.props;
return intl(data, params);
}
}
export function createIntl(
instance: string | object,
): {
intl(id: string, params?: object): ReactNode;
intlString(id: string, params?: object): string;
intlNode(id: string, params?: object): ReactNode;
intl(id: string, params?: object): string;
getLocale(): string;
setLocale(locale: string): void;
} {
let lastLocale: string | undefined;
let data: any = {};
function useLocale(locale: string) {
lastLocale = locale;
const data = computed(() => {
const locale = globalLocale.getLocale();
if (typeof instance === 'string') {
if ((window as any)[instance]) {
data = (window as any)[instance][locale] || {};
data.messages = (window as any)[instance][locale] || {};
} else {
const key = `${instance}_${locale.toLocaleLowerCase()}`;
data = (window as any)[key] || {};
data.messages = (window as any)[key] || {};
}
} else if (instance && typeof instance === 'object') {
data = (instance as any)[locale] || {};
data.messages = (instance as any)[locale] || {};
}
}
});
useLocale(globalLocale.getLocale());
function intlString(key: string, params?: object): string {
function intl(key: string, params?: object): string {
// TODO: tries lost language
const str = data[key];
@ -115,30 +116,22 @@ export function createIntl(
return `##intl@${key}##`;
}
return injectVars(str, params);
return injectVars(str, params, globalLocale.getLocale());
}
class Intl extends PureComponent<{ id: string; params?: object }> {
private dispose = globalLocale.onLocaleChange(locale => {
if (lastLocale !== locale) {
useLocale(locale);
this.forceUpdate();
}
});
componentWillUnmount() {
this.dispose();
}
@observer
class IntlElement extends Component<{ id: string; params?: object }> {
render() {
const { id, params } = this.props;
return intlString(id, params);
return intl(id, params);
}
}
return {
intl(id: string, params?: object) {
return <Intl id={id} params={params} />;
intlNode(id: string, params?: object) {
return createElement(IntlElement, { id, params });
},
intlString,
intl,
getLocale() {
return globalLocale.getLocale();
},

View File

@ -1,84 +0,0 @@
import React, { createRef, PureComponent } from 'react';
import EditorContext from './context';
import { I18nFunction, PluginProps, PluginClass, Plugin } from './definitions';
import Editor from './editor';
import { acceptsRef, generateI18n, isEmpty, transformToPromise } from './utils';
export default function pluginFactory(Comp: PluginClass): React.ComponentType<PluginProps> {
class LowcodePlugin extends PureComponent<PluginProps> {
public static displayName = 'LowcodeEditorPlugin';
public static contextType = EditorContext;
public static init = Comp.init;
public ref: React.RefObject<React.ReactElement> & Plugin;
private editor: Editor;
private pluginKey: string;
private i18n: I18nFunction;
constructor(props, context) {
super(props, context);
if (isEmpty(props.config) || !props.config.pluginKey) {
console.warn('lowcode editor plugin has wrong config');
return;
}
const { editor } = props;
this.ref = createRef<React.ReactElement>();
// 注册插件
this.editor = editor;
this.pluginKey = props.config.pluginKey;
const defaultProps = Comp.defaultProps || {};
const locale = this.editor.get('locale') || defaultProps.locale || 'zh-CN';
const editorMessages = this.editor.get('messages') || {};
const messages = editorMessages[this.pluginKey] || defaultProps.messages || {};
this.i18n = generateI18n(locale, messages);
editor.set('plugins', {
...editor.plugins,
[this.pluginKey]: this,
});
}
public componentWillUnmount(): void {
// 销毁插件
if (this.pluginKey && this.editor && this.editor.plugins) {
delete this.editor.plugins[this.pluginKey];
}
}
public open = (): Promise<any> => {
if (this.ref && this.ref.open && typeof this.ref.open === 'function') {
return transformToPromise(this.ref.open());
}
return Promise.resolve();
};
public close = (): Promise<any> => {
if (this.ref && this.ref.close && typeof this.ref.close === 'function') {
return transformToPromise(this.ref.close());
}
return Promise.resolve();
};
public render(): React.ReactNode {
const { config } = this.props;
const props = {
i18n: this.i18n,
editor: this.editor,
config,
...config.pluginProps,
};
if (acceptsRef(Comp)) {
props.ref = this.ref;
}
return <Comp {...props} />;
}
}
return LowcodePlugin;
}

View File

@ -1,273 +0,0 @@
import IntlMessageFormat from 'intl-messageformat';
import keymaster from 'keymaster';
import _clone from 'lodash/cloneDeep';
import _debounce from 'lodash/debounce';
import _isEmpty from 'lodash/isEmpty';
import _deepEqual from 'lodash/isEqualWith';
import _pick from 'lodash/pick';
import _throttle from 'lodash/throttle';
import _serialize from 'serialize-javascript';
export { get, post, request } from './request';
import Editor from './editor';
import { EditorConfig, I18nFunction, I18nMessages, LocaleType, ShortCutsConfig } from './definitions';
export const pick = _pick;
export const deepEqual = _deepEqual;
export const clone = _clone;
export const isEmpty = _isEmpty;
export const throttle = _throttle;
export const debounce = _debounce;
export const serialize = _serialize;
const ENV = {
TBE: 'TBE',
WEBIDE: 'WEB-IDE',
VSCODE: 'VSCODE',
WEB: 'WEB',
};
declare global {
interface Window {
sendIDEMessage?: (params: IDEMessageParams) => void;
goldlog?: {
record: (logKey: string, gmKey: string, goKey: string, method: 'POST' | 'GET') => (...args: any[]) => any;
};
is_theia?: boolean;
vscode?: boolean;
}
}
export interface IDEMessageParams {
action: string;
data: {
logKey: string;
gmKey: string;
goKey: string;
};
}
/*
*
*/
export function generateI18n(locale: LocaleType = 'zh-CN', messages: I18nMessages = {}): I18nFunction {
return (key: string, values): string => {
if (!messages || !messages[key]) {
return '';
}
const formater = new IntlMessageFormat(messages[key], locale);
return formater.format(values);
};
}
/**
*
*/
export function serializeParams(obj: object): string {
if (typeof obj !== 'object') {
return '';
}
const res: string[] = [];
Object.entries(obj).forEach(([key, val]): void => {
if (val === null || val === undefined || val === '') {
return;
}
if (typeof val === 'object') {
res.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`);
} else {
res.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`);
}
});
return res.join('&');
}
/**
*
* @param {String} gmKey
* @param {Object} params
* @param {String} logKey
*/
export function goldlog(gmKey: string, params: object = {}, logKey: string = 'other'): void {
const sendIDEMessage = window.sendIDEMessage || window.parent.sendIDEMessage;
const goKey = serializeParams({
env: getEnv(),
...params,
});
if (sendIDEMessage) {
sendIDEMessage({
action: 'goldlog',
data: {
logKey: `/iceluna.core.${logKey}`,
gmKey,
goKey,
},
});
}
if (window.goldlog) {
window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST');
}
}
/**
*
*/
export function getEnv(): string {
const userAgent = navigator.userAgent;
const isVscode = /Electron\//.test(userAgent);
if (isVscode) {
return ENV.VSCODE;
}
const isTheia = window.is_theia === true;
if (isTheia) {
return ENV.WEBIDE;
}
return ENV.WEB;
}
// 注册快捷键
export function registShortCuts(config: ShortCutsConfig, editor: Editor): void {
(config || []).forEach((item): void => {
keymaster(item.keyboard, (ev: Event): void => {
ev.preventDefault();
item.handler(editor, ev, keymaster);
});
});
}
// 取消注册快捷
export function unRegistShortCuts(config: ShortCutsConfig): void {
(config || []).forEach((item): void => {
keymaster.unbind(item.keyboard);
});
if (window.parent.vscode) {
keymaster.unbind('command+c');
keymaster.unbind('command+v');
}
}
/**
* promise形式bool类型判断是reject还是resolveresolve
*/
export function transformToPromise(input: any): Promise<{}> {
if (input instanceof Promise) {
return input;
}
return new Promise((resolve, reject): void => {
if (input || input === undefined) {
resolve();
} else {
reject();
}
});
}
/**
* Map类型
*/
interface MapOf<T> {
[propName: string]: T;
}
export function transformArrayToMap<T>(arr: T[], key: string, overwrite: boolean = true): MapOf<T> {
if (isEmpty(arr) || !Array.isArray(arr)) {
return {};
}
const res = {};
arr.forEach((item): void => {
const curKey = item[key];
if (item[key] === undefined) {
return;
}
if (res[curKey] && !overwrite) {
return;
}
res[curKey] = item;
});
return res;
}
/**
* url的查询参数
*/
interface Query {
[propName: string]: string;
}
export function parseSearch(search: string): Query {
if (!search || typeof search !== 'string') {
return {};
}
const str = search.replace(/^\?/, '');
const paramStr = str.split('&');
const res = {};
paramStr.forEach((item): void => {
const regRes = item.split('=');
if (regRes[0] && regRes[1]) {
res[regRes[0]] = decodeURIComponent(regRes[1]);
}
});
return res;
}
export function comboEditorConfig(defaultConfig: EditorConfig = {}, customConfig: EditorConfig): EditorConfig {
const { skeleton, theme, plugins, hooks, shortCuts, lifeCycles, constants, utils, i18n } = customConfig || {};
if (skeleton && skeleton.handler && typeof skeleton.handler === 'function') {
return skeleton.handler({
skeleton,
...defaultConfig,
});
}
const defaultShortCuts = transformArrayToMap(defaultConfig.shortCuts || [], 'keyboard');
const customShortCuts = transformArrayToMap(shortCuts || [], 'keyboard');
const localeList = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP'];
const i18nConfig = {};
localeList.forEach((key): void => {
i18nConfig[key] = {
...(defaultConfig.i18n && defaultConfig.i18n[key]),
...(i18n && i18n[key]),
};
});
return {
skeleton,
theme: {
...defaultConfig.theme,
...theme,
},
plugins: {
...defaultConfig.plugins,
...plugins,
},
hooks: [...(defaultConfig.hooks || []), ...(hooks || [])],
shortCuts: Object.values({
...defaultShortCuts,
...customShortCuts,
}),
lifeCycles: {
...defaultConfig.lifeCycles,
...lifeCycles,
},
constants: {
...defaultConfig.constants,
...constants,
},
utils: [...(defaultConfig.utils || []), ...(utils || [])],
i18n: i18nConfig,
};
}
/**
* ref
* @param {*} Comp
*/
export function acceptsRef(Comp: React.ReactNode): boolean {
const hasSymbol = typeof Symbol === 'function' && Symbol.for;
const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0;
if (!Comp || typeof Comp !== 'object' || isEmpty(Comp)) {
return false;
}
return (
(Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent)
);
}

View File

@ -0,0 +1,11 @@
/**
*
* @param {String} gmKey
* @param {Object} params
* @param {String} logKey
*/
export function goldlog(gmKey: string, params: object = {}, logKey: string = 'other'): void {
}

View File

@ -0,0 +1,4 @@
export * from './get-public-path';
export * from './goldlog';
export * from './obx';
export * from './request';

View File

@ -1,8 +1,10 @@
import 'whatwg-fetch';
import Debug from 'debug';
const debug = Debug('request');
export function serialize(obj: object): string {
export function serialize(obj?: object): string {
if (!obj) {
return '';
}
const rst: string[] = [];
Object.entries(obj || {}).forEach(([key, val]): void => {
if (val === null || val === undefined || val === '') return;
@ -12,7 +14,7 @@ export function serialize(obj: object): string {
return rst.join('&');
}
export function buildUrl(dataAPI: string, params: object): string {
export function buildUrl(dataAPI: string, params?: object): string {
const paramStr = serialize(params);
if (paramStr) {
return dataAPI.indexOf('?') > 0 ? `${dataAPI}&${paramStr}` : `${dataAPI}?${paramStr}`;
@ -25,7 +27,7 @@ export function get(dataAPI: string, params?: object, headers?: object, otherPro
Accept: 'application/json',
...headers,
};
return request(buildUrl(dataAPI, params), 'GET', null, fetchHeaders, otherProps);
return request(buildUrl(dataAPI, params), 'GET', undefined, fetchHeaders, otherProps);
}
export function post(dataAPI: string, params?: object, headers?: object, otherProps?: object): Promise<any> {
@ -50,7 +52,7 @@ export function request(
method: string = 'GET',
data?: object | string,
headers?: object,
otherProps?: object,
otherProps?: any,
): Promise<any> {
return new Promise((resolve, reject): void => {
if (otherProps && otherProps.timeout) {
@ -109,7 +111,7 @@ export function request(
return null;
}
})
.then((json: object): void => {
.then((json: any): void => {
if (json && json.__success !== false) {
resolve(json);
} else {

View File

@ -1,3 +1,3 @@
// TODO move another place
export * from './tip';
export * from './title';
export * from './svg-icon';

View File

@ -0,0 +1,4 @@
import './style.less';
export * from './tip';
export * from './tip-container';

View File

@ -1,8 +1,8 @@
import { Component } from 'react';
import Tip from './tip';
import tipHandler from './tip-handler';
import { TipItem } from './tip-item';
import { tipHandler } from './tip-handler';
export default class TipContainer extends Component {
export class TipContainer extends Component {
shouldComponentUpdate() {
return false;
}
@ -29,7 +29,7 @@ export default class TipContainer extends Component {
render() {
return (
<div className="lc-tips-container">
<Tip />
<TipItem />
</div>
);
}

View File

@ -1,54 +1,10 @@
import { EventEmitter } from 'events';
import { TipConfig } from '../../types';
import { TipConfig } from '@ali/lowcode-types';
export interface TipOptions extends TipConfig {
target: HTMLElement;
}
function findTip(target: HTMLElement | null): TipOptions | null {
if (!target) {
return null;
}
// optimize deep finding on mouseover
let loopupLimit = 10;
while (target && loopupLimit-- > 0) {
// get tip from target node
if (target.dataset && target.dataset.tip) {
return {
children: target.dataset.tip,
direction: (target.dataset.direction || target.dataset.dir) as any,
theme: target.dataset.theme,
target,
};
}
// or get tip from child nodes
let child: HTMLElement | null = target.lastElementChild as HTMLElement;
while (child) {
if (child.dataset && child.dataset.role === 'tip') {
const tipId = child.dataset.tipId;
if (!tipId) {
return null;
}
const tipProps = tipsMap.get(tipId);
if (!tipProps) {
return null;
}
return {
...tipProps,
target,
};
}
child = child.previousElementSibling as HTMLElement;
}
target = target.parentNode as HTMLElement;
}
return null;
}
class TipHandler {
tip: TipOptions | null = null;
private showDelay: number | null = null;
@ -60,7 +16,7 @@ class TipHandler {
if (tip) {
if (this.tip) {
// the some target should return
if (this.tip.target === tip.target) {
if ((this.tip as any).target === (tip as any).target) {
this.tip = tip;
return;
}
@ -127,13 +83,57 @@ class TipHandler {
}
}
export const tipHandler = new TipHandler();
function findTip(target: HTMLElement | null): TipOptions | null {
if (!target) {
return null;
}
// optimize deep finding on mouseover
let loopupLimit = 10;
while (target && loopupLimit-- > 0) {
// get tip from target node
if (target.dataset && target.dataset.tip) {
return {
children: target.dataset.tip,
direction: (target.dataset.direction || target.dataset.dir) as any,
theme: target.dataset.theme,
target,
};
}
// or get tip from child nodes
let child: HTMLElement | null = target.lastElementChild as HTMLElement;
while (child) {
if (child.dataset && child.dataset.role === 'tip') {
const tipId = child.dataset.tipId;
if (!tipId) {
return null;
}
const tipProps = tipsMap.get(tipId);
if (!tipProps) {
return null;
}
return {
...tipProps,
target,
};
}
child = child.previousElementSibling as HTMLElement;
}
target = target.parentNode as HTMLElement;
}
return null;
}
const tipsMap = new Map<string, TipConfig>();
export function saveTips(id: string, props: TipConfig | null) {
export function postTip(id: string, props: TipConfig | null) {
if (props) {
tipsMap.set(id, props);
} else {
tipsMap.delete(id);
}
}
export default new TipHandler();

View File

@ -1,10 +1,11 @@
import { Component } from 'react';
import classNames from 'classnames';
import { intl } from '@ali/lowcode-editor-core';
import { TipConfig } from '@ali/lowcode-types';
import { resolvePosition } from './utils';
import tipHandler, { TipOptions } from './tip-handler';
import { intl } from '../../intl';
import { tipHandler } from './tip-handler';
export default class Tip extends Component {
export class TipItem extends Component {
private dispose?: () => void;
constructor(props: any) {
super(props);
@ -101,7 +102,7 @@ export default class Tip extends Component {
}
render() {
const tip: TipOptions = tipHandler.tip || ({} as any);
const tip: TipConfig = tipHandler.tip || ({} as any);
const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null);
this.originClassName = className;

View File

@ -0,0 +1,17 @@
import { Component } from 'react';
import { TipConfig } from '@ali/lowcode-types';
import { uniqueId } from '@ali/lowcode-utils';
import { postTip } from './tip-handler';
export class Tip extends Component<TipConfig> {
private id = uniqueId('tips$');
componentWillUnmount() {
postTip(this.id, null);
}
render() {
postTip(this.id, this.props);
return <meta data-role="tip" data-tip-id={this.id} />;
}
}

View File

@ -1,10 +1,10 @@
import { Component, isValidElement } from 'react';
import classNames from 'classnames';
import EmbedTip from '../tip/embed-tip';
import './title.less';
import { createIcon } from '../../utils';
import { TitleContent, isI18nData } from '../../types';
import { createIcon } from '@ali/lowcode-utils';
import { TitleContent, isI18nData } from '@ali/lowcode-types';
import { intl } from '../../intl';
import { Tip } from '../tip';
import './title.less';
export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> {
render() {
@ -20,14 +20,14 @@ export class Title extends Component<{ title: TitleContent; className?: string;
let tip: any = null;
if (title.tip) {
if (isValidElement(title.tip) && title.tip.type === EmbedTip) {
if (isValidElement(title.tip) && title.tip.type === Tip) {
tip = title.tip;
} else {
const tipProps =
typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip))
? title.tip
: { children: title.tip };
tip = <EmbedTip direction="top" theme="black" {...tipProps} />;
tip = <Tip direction="top" theme="black" {...tipProps} />;
}
}

View File

@ -3,7 +3,5 @@
"compilerOptions": {
"outDir": "lib"
},
"include": [
"./src/"
]
"include": ["./src/"],
}

View File

@ -0,0 +1,59 @@
import { obx, computed } from '@ali/lowcode-editor-core';
import WidgetContainer from './widget/widget-container';
import { Skeleton } from './skeleton';
import { IWidget } from './widget/widget';
import { IWidgetBaseConfig } from './types';
export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = IWidget> {
@obx private _visible: boolean = true;
@computed get visible() {
if (this.exclusive) {
return this.container.current != null;
}
return this._visible;
}
get current() {
if (this.exclusive) {
return this.container.current;
}
return null;
}
readonly container: WidgetContainer<T, C>;
constructor(readonly skeleton: Skeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent: boolean = false) {
this.container = skeleton.createContainer(name, handle, exclusive, () => this.visible, defaultSetCurrent);
}
@computed isEmpty(): boolean {
return this.container.items.length < 1;
}
add(config: T | C): T {
return this.container.add(config);
}
private lastCurrent: T | null = null;
setVisible(flag: boolean) {
if (this.exclusive) {
const current = this.container.current;
if (flag && !current) {
this.container.active(this.lastCurrent || this.container.getAt(0))
} else if (current) {
this.lastCurrent = current;
this.container.unactive(current);
}
return;
}
this._visible = flag;
}
hide() {
this.setVisible(false);
}
show() {
this.setVisible(true);
}
}

View File

@ -1,59 +0,0 @@
.lowcode-left-plugin {
font-size: 20px;
text-align: center;
line-height: 44px;
height: 44px;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
color: $color-text1-3;
&.locked {
color: red !important;
}
&:hover {
color: $color-brand1-6;
&:before {
content: attr(data-tooltip);
display: block;
position: absolute;
left: 45px;
top: 8px;
line-height: 18px;
font-size: 12px;
white-space: nowrap;
padding: 6px 8px;
border-radius: 4px;
background: $balloon-normal-color-bg;
border: 1px solid $balloon-normal-color-border;
color: $color-text1-3;
z-index: 100;
}
&:after {
content: '';
display: block;
position: absolute;
width: 10px;
height: 10px;
transform: rotate(45deg);
left: 40px;
top: 18px;
background: $balloon-normal-color-bg;
border-left: 1px solid $balloon-normal-color-border;
border-bottom: 1px solid $balloon-normal-color-border;
z-index: 100;
}
}
&.active {
color: $color-brand1-9;
&.disabled {
color: $color-text1-1;
}
&:hover {
color: $color-brand1-6;
}
}
&.disabled {
cursor: not-allowed;
color: $color-text1-1;
}
}

View File

@ -1,221 +0,0 @@
import React, { PureComponent, Fragment } from 'react';
import classNames from 'classnames';
import { Balloon, Dialog, Icon, Badge } from '@alifd/next';
import Editor from '@ali/lowcode-editor-core';
import {
PluginConfig,
PluginClass,
} from '@ali/lowcode-editor-core/lib/definitions';
import './index.scss';
export interface LeftPluginProps {
active?: boolean;
config: PluginConfig;
disabled?: boolean;
editor: Editor;
locked?: boolean;
marked?: boolean;
onClick?: () => void;
pluginClass: PluginClass | undefined;
}
export interface LeftPluginState {
dialogVisible: boolean;
}
export default class LeftPlugin extends PureComponent<
LeftPluginProps,
LeftPluginState
> {
static displayName = 'LowcodeLeftPlugin';
static defaultProps = {
active: false,
config: {},
disabled: false,
marked: false,
locked: false,
onClick: (): void => {},
};
constructor(props, context) {
super(props, context);
this.state = {
dialogVisible: false,
};
}
componentDidMount(): void {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (editor && pluginKey) {
editor.on(`${pluginKey}.dialog.show`, this.handleShow);
editor.on(`${pluginKey}.dialog.close`, this.handleClose);
}
}
componentWillUnmount(): void {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (editor && pluginKey) {
editor.off(`${pluginKey}.dialog.show`, this.handleShow);
editor.off(`${pluginKey}.dialog.close`, this.handleClose);
}
}
handleClose = (): void => {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
const plugin = editor.plugins && editor.plugins[pluginKey];
if (plugin) {
plugin.close().then((): void => {
this.setState({
dialogVisible: false,
});
});
}
};
handleOpen = (): void => {
// todo 对话框类型的插件初始时拿不到插件实例
this.setState({
dialogVisible: true,
});
};
handleShow = (): void => {
const { disabled, config, onClick, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (disabled || !pluginKey) return;
this.handleOpen();
// 考虑到弹窗情况,延时发送消息
setTimeout((): void => {
editor.emit(`${pluginKey}.plugin.activate`);
}, 0);
if (onClick) {
onClick();
}
};
renderIcon = (clickCallback): React.ReactNode => {
const { active, disabled, marked, locked, onClick, config } = this.props;
const { pluginKey, props } = config || {};
const { icon, title } = props || {};
return (
<div
className={classNames('lowcode-left-plugin', pluginKey, {
active,
disabled,
locked,
})}
data-tooltip={title}
onClick={(): void => {
if (disabled) return;
// 考虑到弹窗情况,延时发送消息
clickCallback && clickCallback();
onClick && onClick();
}}
>
{marked ? (
<Badge dot>
<Icon type={icon} size="small" />
</Badge>
) : (
<Icon type={icon} size="small" />
)}
</div>
);
};
render(): React.ReactNode {
const {
marked,
locked,
active,
disabled,
config,
editor,
pluginClass: Comp,
} = this.props;
const { pluginKey, props, type, pluginProps } = config || {};
const { onClick, title } = props || {};
const { dialogVisible } = this.state;
if (!pluginKey || !type || !props) return null;
const node = Comp ? (
<Comp
editor={editor}
active={active}
locked={locked}
disabled={disabled}
config={config}
onClick={(): void => {
onClick && onClick.call(null, editor);
}}
{...pluginProps}
/>
) : null;
switch (type) {
case 'LinkIcon':
return (
<a {...(props.linkProps || {})}>
{this.renderIcon((): void => {
onClick && onClick.call(null, editor);
})}
</a>
);
case 'Icon':
return this.renderIcon((): void => {
onClick && onClick.call(null, editor);
});
case 'DialogIcon':
return (
<Fragment>
{this.renderIcon((): void => {
onClick && onClick.call(null, editor);
this.handleOpen();
})}
<Dialog
onOk={(): void => {
editor.emit(`${pluginKey}.dialog.onOk`);
this.handleClose();
}}
onCancel={this.handleClose}
onClose={this.handleClose}
title={title}
style={{
width: 500,
...(props.dialogProps && props.dialogProps.style),
}}
{...(props.dialogProps || {})}
visible={dialogVisible}
>
{node}
</Dialog>
</Fragment>
);
case 'BalloonIcon':
return (
<Balloon
trigger={this.renderIcon((): void => {
onClick && onClick.call(null, editor);
})}
align="r"
triggerType={['click', 'hover']}
{...(props.balloonProps || {})}
>
{node}
</Balloon>
);
case 'PanelIcon':
return this.renderIcon((): void => {
onClick && onClick.call(null, editor);
});
case 'Custom':
return marked ? <Badge dot>{node}</Badge> : node;
default:
return null;
}
}
}

View File

@ -1,52 +0,0 @@
.lowcode-panel {
user-select: none;
overflow: hidden;
position: relative;
background: $card-background;
transition: width 0.3s ease;
transform: translate3d(0, 0, 0);
height: 100%;
&.visible {
border-right: 1px solid $color-line1-1;
}
.drag-area {
display: none;
}
&.floatable {
position: absolute;
top: 0;
bottom: 0;
z-index: 999;
}
&.draggable {
.drag-area {
display: block;
width: 10px;
position: absolute;
top: 0;
bottom: 0;
cursor: col-resize;
z-index: 9999;
}
&.left {
.drag-area {
right: 0;
}
}
&.right {
.drag-area {
left: 0;
}
}
}
&.left {
&.floatable {
left: 50px;
}
}
&.right {
&.floatable {
right: 48px;
}
}
}

View File

@ -1,60 +0,0 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import './index.scss';
export interface PanelProps {
align: 'left' | 'right';
defaultWidth: number;
minWidth: number;
draggable: boolean;
floatable: boolean;
children: Plugin;
visible: boolean;
}
export interface PanelState {
width: number;
}
export default class Panel extends PureComponent<PanelProps, PanelState> {
static displayName = 'LowcodePanel';
static defaultProps = {
align: 'left',
defaultWidth: 240,
minWidth: 100,
draggable: true,
floatable: false,
visible: true,
};
constructor(props) {
super(props);
this.state = {
width: props.defaultWidth,
};
}
render(): React.ReactNode {
const { align, draggable, floatable, visible } = this.props;
const { width } = this.state;
return (
<div
className={classNames('lowcode-panel', align, {
draggable,
floatable,
visible,
})}
style={{
width,
display: visible ? '' : 'none',
}}
>
{this.props.children}
<div className="drag-area" />
</div>
);
}
}

View File

@ -1,77 +0,0 @@
.lowcode-top-icon {
display: inline-block;
width: 44px;
font-size: 20px;
line-height: 48px;
color: $color-text1-3;
position: relative;
&.disabled {
cursor: not-allowed;
color: $color-text1-1;
&:hover {
color: $color-text1-1;
}
}
&.active {
color: $color-brand1-9;
&:hover {
color: $color-brand1-6;
}
}
&.locked {
color: red !important;
}
&:hover {
color: $color-brand1-6;
&:before {
content: attr(data-tooltip);
display: block;
height: auto;
width: auto;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
bottom: -32px;
line-height: 18px;
font-size: 12px;
white-space: nowrap;
padding: 6px 8px;
border-radius: 4px;
background: $balloon-normal-color-bg;
border: 1px solid $balloon-normal-color-border;
color: $color-text1-3;
z-index: 100;
}
&:after {
content: '';
display: block;
position: absolute;
width: 10px;
height: 10px;
left: 50%;
transform: translate(-50%, 0) rotate(45deg);
bottom: -5px;
background: $balloon-normal-color-bg;
border-left: 1px solid $balloon-normal-color-border;
border-top: 1px solid $balloon-normal-color-border;
opacity: 1;
visibility: visible;
z-index: 100;
}
}
i.next-icon {
&:before {
font-size: 16px;
}
margin-right: 0;
line-height: 18px;
}
span {
display: block;
margin: 0px -5px 0;
line-height: 16px;
text-align: center;
font-size: 12px;
transform: scale(0.8);
}
}

View File

@ -1,63 +0,0 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { Icon } from '@alifd/next';
import './index.scss';
export interface TopIconProps {
active?: boolean;
className?: string;
disabled?: boolean;
icon: string;
id?: string;
locked?: boolean;
marked?: boolean;
onClick?: () => void;
style?: React.CSSProperties;
title?: string;
}
export default class TopIcon extends PureComponent<TopIconProps> {
static displayName = 'LowcodeTopIcon';
static defaultProps = {
active: false,
className: '',
disabled: false,
icon: '',
id: '',
locked: false,
onClick: (): void => {},
style: {},
title: '',
};
render(): React.ReactNode {
const {
active,
disabled,
icon,
locked,
title,
className,
id,
style,
onClick,
} = this.props;
return (
<div
className={classNames('lowcode-top-icon', className, {
active,
disabled,
locked,
})}
data-tooltip={title}
id={id}
style={style}
onClick={disabled ? undefined : onClick}
>
<Icon type={icon} />
</div>
);
}
}

View File

@ -1,2 +0,0 @@
.lowcode-top-addon {
}

View File

@ -1,219 +0,0 @@
import React, { PureComponent, Fragment } from 'react';
import { Balloon, Badge, Dialog } from '@alifd/next';
import Editor from '@ali/lowcode-editor-core';
import {
PluginConfig,
PluginClass,
} from '@ali/lowcode-editor-core/lib/definitions';
import TopIcon from '../TopIcon';
import './index.scss';
export interface TopPluginProps {
active?: boolean;
config: PluginConfig;
disabled?: boolean;
editor: Editor;
locked?: boolean;
marked?: boolean;
onClick?: () => void;
pluginClass: PluginClass | undefined;
}
export interface TopPluginState {
dialogVisible: boolean;
}
export default class TopPlugin extends PureComponent<
TopPluginProps,
TopPluginState
> {
static displayName = 'LowcodeTopPlugin';
static defaultProps = {
active: false,
config: {},
disabled: false,
marked: false,
locked: false,
onClick: (): void => {},
};
constructor(props, context) {
super(props, context);
this.state = {
dialogVisible: false,
};
}
componentDidMount(): void {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (editor && pluginKey) {
editor.on(`${pluginKey}.dialog.show`, this.handleShow);
editor.on(`${pluginKey}.dialog.close`, this.handleClose);
}
}
componentWillUnmount(): void {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (editor && pluginKey) {
editor.off(`${pluginKey}.dialog.show`, this.handleShow);
editor.off(`${pluginKey}.dialog.close`, this.handleClose);
}
}
handleShow = (): void => {
const { disabled, config, onClick, editor } = this.props;
const pluginKey = config && config.pluginKey;
if (disabled || !pluginKey) return;
this.handleOpen();
// 考虑到弹窗情况,延时发送消息
setTimeout((): void => {
editor.emit(`${pluginKey}.plugin.activate`);
}, 0);
onClick && onClick();
};
handleClose = (): void => {
const { config, editor } = this.props;
const pluginKey = config && config.pluginKey;
const plugin = editor.plugins && editor.plugins[pluginKey];
if (plugin) {
plugin.close().then((): void => {
this.setState({
dialogVisible: false,
});
});
}
};
handleOpen = (): void => {
// todo dialog类型的插件初始时拿不动插件实例
this.setState({
dialogVisible: true,
});
};
renderIcon = (clickCallback): React.ReactNode => {
const {
active,
disabled,
marked,
locked,
config,
onClick,
editor,
} = this.props;
const { pluginKey, props } = config || {};
const { icon, title } = props || {};
const node = (
<TopIcon
className={`lowcode-top-plugin ${pluginKey}`}
active={active}
disabled={disabled}
locked={locked}
icon={icon}
title={title}
onClick={(): void => {
if (disabled) return;
// 考虑到弹窗情况,延时发送消息
setTimeout((): void => {
editor.emit(`${pluginKey}.plugin.activate`);
}, 0);
clickCallback && clickCallback();
onClick && onClick();
}}
/>
);
return marked ? <Badge dot>{node}</Badge> : node;
};
render(): React.ReactNode {
const {
active,
marked,
locked,
disabled,
config,
editor,
pluginClass: Comp,
} = this.props;
const { pluginKey, pluginProps, props, type } = config || {};
const { onClick, title } = props || {};
const { dialogVisible } = this.state;
if (!pluginKey || !type) return null;
const node = Comp ? (
<Comp
editor={editor}
active={active}
locked={locked}
disabled={disabled}
config={config}
onClick={(): void => {
onClick && onClick.call(null, editor);
}}
{...pluginProps}
/>
) : null;
switch (type) {
case 'LinkIcon':
return (
<a {...props.linkProps}>
{this.renderIcon((): void => {
onClick && onClick.call(null, editor);
})}
</a>
);
case 'Icon':
return this.renderIcon((): void => {
onClick && onClick.call(null, editor);
});
case 'DialogIcon':
return (
<Fragment>
{this.renderIcon((): void => {
onClick && onClick.call(null, editor);
this.handleOpen();
})}
<Dialog
onOk={(): void => {
editor.emit(`${pluginKey}.dialog.onOk`);
this.handleClose();
}}
onCancel={this.handleClose}
onClose={this.handleClose}
title={title}
style={{
width: 500,
...(props.dialogProps && props.dialogProps.style),
}}
{...props.dialogProps}
visible={dialogVisible}
>
{node}
</Dialog>
</Fragment>
);
case 'BalloonIcon':
return (
<Balloon
trigger={this.renderIcon((): void => {
onClick && onClick.call(null, editor);
})}
triggerType={['click', 'hover']}
{...props.balloonProps}
>
{node}
</Balloon>
);
case 'Custom':
return marked ? <Badge dot>{node}</Badge> : node;
default:
return null;
}
}
}

View File

@ -0,0 +1,5 @@
* 拖拽排序有问题
* forceInline 有问题
* 部分改变不响应
* 样式还原
* autofocus

View File

@ -0,0 +1,280 @@
import { Component, Fragment } from 'react';
import { Icon, Button, Message } from '@alifd/next';
import { Title } from '@ali/lowcode-editor-core';
import { SetterType, FieldConfig, SetterConfig } from '@ali/lowcode-types';
import { SettingField } from '@ali/lowcode-designer';
import { createSettingFieldView } from '../settings/settings-pane';
import { PopupContext, PopupPipe } from '../popup';
import Sortable from './sortable';
import './style.less';
interface ArraySetterState {
items: SettingField[];
itemsMap: Map<string | number, SettingField>;
prevLength: number;
}
interface ArraySetterProps {
value: any[];
field: SettingField;
itemSetter?: SetterType;
columns?: FieldConfig[];
multiValue?: boolean;
}
export class ListSetter extends Component<ArraySetterProps, ArraySetterState> {
static getDerivedStateFromProps(props: ArraySetterProps, state: ArraySetterState) {
const { value, field } = props;
const newLength = value && Array.isArray(value) ? value.length : 0;
if (state && state.prevLength === newLength) {
return null;
}
// props value length change will go here
const originLength = state ? state.items.length : 0;
if (state && originLength === newLength) {
return {
prevLength: newLength,
};
}
const itemsMap = state ? state.itemsMap : new Map<string | number, SettingField>();
let items = state ? state.items.slice() : [];
if (newLength > originLength) {
for (let i = originLength; i < newLength; i++) {
const item = field.createField({
name: i,
setter: props.itemSetter,
// FIXME:
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,
};
}
state: ArraySetterState = {
items: [],
itemsMap: new Map<string | number, SettingField>(),
prevLength: 0,
};
onSort(sortedIds: Array<string | number>) {
const { itemsMap } = this.state;
const items = sortedIds.map((id, index) => {
const item = itemsMap.get(id)!;
item.setKey(index);
return item;
});
this.setState({
items,
});
}
private scrollToLast: boolean = false;
onAdd() {
const { items, itemsMap } = this.state;
const { itemSetter } = this.props;
const initialValue = typeof itemSetter === 'object' ? (itemSetter as any).initialValue : null;
const item = this.props.field.createField({
name: items.length,
setter: itemSetter,
// FIXME:
forceInline: 1,
});
items.push(item);
itemsMap.set(item.id, item);
item.setValue(typeof initialValue === 'function' ? initialValue(item) : initialValue);
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() {
let columns: any = null;
if (this.props.columns) {
columns = this.props.columns.map((column) => <Title key={column.name} title={column.title || (column.name as string)} />);
}
const { items } = this.state;
const scrollToLast = this.scrollToLast;
this.scrollToLast = false;
const lastIndex = items.length - 1;
const content =
items.length > 0 ? (
<div className="lc-setter-list-scroll-body">
<Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}>
{items.map((field, index) => (
<ArrayItem
key={field.id}
scrollIntoView={scrollToLast && index === lastIndex}
field={field}
onRemove={this.onRemove.bind(this, field)}
/>
))}
</Sortable>
</div>
) : this.props.multiValue ? (
<Message type="warning"></Message>
) : (
<Message type="notice"></Message>
);
return (
<div className="lc-setter-list lc-block-setter">
{/*<div className="lc-block-setter-actions">
<Button size="medium" onClick={this.onAdd.bind(this)}>
<Icon type="add" />
<span></span>
</Button>
</div>*/}
{columns && <div className="lc-setter-list-columns">{columns}</div>}
{content}
<Button className="lc-setter-list-add" type="primary" onClick={this.onAdd.bind(this)}>
<Icon type="add" />
<span></span>
</Button>
</div>
);
}
}
class ArrayItem extends Component<{
field: SettingField;
onRemove: () => void;
scrollIntoView: boolean;
}> {
shouldComponentUpdate() {
return false;
}
private shell?: HTMLDivElement | null;
componentDidMount() {
if (this.props.scrollIntoView && this.shell) {
this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
render() {
const { onRemove, field } = this.props;
return (
<div className="lc-listitem" ref={(ref) => (this.shell = ref)}>
<div draggable className="lc-listitem-handler">
<Icon type="ellipsis" size="small" />
</div>
<div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div>
<div className="lc-listitem-actions">
<div className="lc-listitem-action" onClick={onRemove}>
<Icon type="ashbin" size="small" />
</div>
</div>
</div>
);
}
}
class TableSetter extends ListSetter {
// todo:
// forceInline = 1
// has more actions
}
export default class ArraySetter extends Component<{
value: any[];
field: SettingField;
itemSetter?: SetterType;
mode?: 'popup' | 'list';
forceInline?: boolean;
multiValue?: boolean;
}> {
static contextType = PopupContext;
private pipe: any;
render() {
const { mode, forceInline, ...props } = this.props;
const { field, itemSetter } = props;
let columns: FieldConfig[] | undefined;
if ((itemSetter as SetterConfig)?.componentName === 'ObjectSetter') {
const items: FieldConfig[] = (itemSetter as any).props?.config?.items;
if (items && Array.isArray(items)) {
columns = items.filter((item) => item.isRequired || item.important || (item.setter as any)?.isRequired);
if (columns.length > 4) {
columns = columns.slice(0, 4);
}
}
}
if (mode === 'popup' || forceInline) {
const title = (
<Fragment>
<Title title={field.title} />
</Fragment>
);
if (!this.pipe) {
let width = 360;
if (columns) {
if (columns.length === 3) {
width = 480;
} else if (columns.length > 3) {
width = 600;
}
}
this.pipe = (this.context as PopupPipe).create({ width });
}
this.pipe.send(<TableSetter key={field.id} {...props} columns={columns} />, title);
return (
<Button
type={forceInline ? 'normal' : 'primary'}
onClick={(e) => {
this.pipe.show((e as any).target, field.id);
}}
>
<Icon type="edit" />
{forceInline ? title : '编辑数组'}
</Button>
);
} else {
return <ListSetter {...props} columns={columns?.slice(0, 2)} />;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,103 @@
.lc-setter-list {
[draggable] {
cursor: move;
}
color: var(--color-text);
.next-btn {
display: inline-flex;
align-items: center;
line-height: 1 !important;
max-width: 100%;
text-overflow: ellipsis;
}
.lc-setter-list-add {
display: block;
width: 100%;
margin-top: 8px;;
}
.lc-setter-list-columns {
display: flex;
> .lc-title {
flex: 1;
justify-content: center;
}
margin-left: 47px;
margin-right: 28px;
margin-bottom: 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;
&:not(:last-child) {
margin-bottom: 5px;
}
.lc-listitem {
position: relative;
outline: none;
display: flex;
align-items: stretch;
height: 34px;
.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: stretch;
overflow: hidden;
min-width: 0;
text-overflow: ellipsis;
.lc-field {
padding: 0 !important;
display: flex;
align-items: center;
>.lc-field-body {
justify-content: center;
}
}
> * {
width: 100%;
}
.next-btn {
display: block;
width: 100%;
}
}
.lc-listitem-handler {
margin-left: 2px;
display: inline-flex;
align-items: center;
.next-icon-ellipsis {
transform: rotate(90deg);
}
opacity: 0.6;
}
}
}
}

View File

@ -0,0 +1,185 @@
import { Component } from 'react';
import classNames from 'classnames';
import { Icon } from '@alifd/next';
import { Title } from '@ali/lowcode-editor-core';
import { TitleContent } from '@ali/lowcode-types';
import { PopupPipe, PopupContext } from '../popup';
import './index.less';
export interface FieldProps {
className?: string;
title?: TitleContent | null;
defaultDisplay?: 'accordion' | 'inline' | 'block';
collapsed?: boolean;
onExpandChange?: (expandState: boolean) => void;
}
export class Field extends Component<FieldProps> {
state = {
collapsed: this.props.collapsed,
display: this.props.defaultDisplay || 'inline',
};
private toggleExpand = () => {
const { onExpandChange } = this.props;
const collapsed = !this.state.collapsed;
this.setState({
collapsed,
});
onExpandChange && onExpandChange(!collapsed);
};
private body: HTMLDivElement | null = null;
private dispose?: () => void;
private deployBlockTesting() {
if (this.dispose) {
this.dispose();
}
const body = this.body;
if (!body) {
return;
}
const check = () => {
const setter = body.firstElementChild;
if (setter && setter.classList.contains('lc-block-setter')) {
this.setState({
display: 'block',
});
} else {
this.setState({
display: 'inline',
});
}
};
const observer = new MutationObserver(check);
check();
observer.observe(body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
});
this.dispose = () => observer.disconnect();
}
componentDidMount() {
const { defaultDisplay } = this.props;
if (!defaultDisplay || defaultDisplay === 'inline') {
this.deployBlockTesting();
}
}
componentWillUnmount() {
if (this.dispose) {
this.dispose();
}
}
render() {
const { className, children, title } = this.props;
const { display, collapsed } = this.state;
const isAccordion = display === 'accordion';
return (
<div
className={classNames(`lc-field lc-${display}-field`, className, {
'lc-field-is-collapsed': isAccordion && collapsed,
})}
>
<div className="lc-field-head" onClick={isAccordion ? this.toggleExpand : undefined}>
<div className="lc-field-title">
<Title title={title || ''} />
</div>
{isAccordion && <Icon className="lc-field-icon" type="arrow-up" size="xs" />}
</div>
<div key="body" ref={(shell) => (this.body = shell)} className="lc-field-body">
{children}
</div>
</div>
);
}
}
export interface PopupFieldProps extends FieldProps {
width?: number;
}
export class PopupField extends Component<PopupFieldProps> {
static contextType = PopupContext;
private pipe: any;
static defaultProps: PopupFieldProps = {
width: 300,
};
render() {
const { className, children, title, width } = this.props;
if (!this.pipe) {
this.pipe = (this.context as PopupPipe).create({ width });
}
const titleElement = title && (
<div className="lc-field-title">
<Title title={title} />
</div>
);
this.pipe.send(<div className="lc-field-body">{children}</div>, titleElement);
return (
<div className={classNames('lc-field lc-popup-field', className)}>
{title && (
<div
className="lc-field-head"
onClick={(e) => {
this.pipe.show((e as any).target);
}}
>
<div className="lc-field-title">
<Title title={title} />
</div>
<Icon className="lc-field-icon" type="arrow-left" size="xs" />
</div>
)}
</div>
);
}
}
export interface EntryFieldProps extends FieldProps {
stageName?: string;
}
export class EntryField extends Component<EntryFieldProps> {
render() {
const { stageName, title, className } = this.props;
const classNameList = classNames('engine-setting-field', 'engine-entry-field', className);
const fieldProps: any = {};
if (stageName) {
// 为 stage 切换奠定基础
fieldProps['data-stage-target'] = stageName;
}
const innerElements = [
<span className="engine-field-title" key="field-title">
{title}
</span>,
// renderTip(tip, { propName }),
// <Icons name="arrow" className="engine-field-arrow" size="12px" key="engine-field-arrow-icon" />,
];
return (
<div className={classNameList} {...fieldProps}>
{innerElements}
</div>
);
}
}
export class PlainField extends Component<FieldProps> {
render() {
const { className, children } = this.props;
return (
<div className={classNames(`lc-field lc-plain-field`, className)}>
<div className="lc-field-body">{children}</div>
</div>
);
}
}

View File

@ -0,0 +1,154 @@
@import '../variables.less';
@x-gap: 10px;
@y-gap: 8px;
.lc-field {
// head
.lc-field-head {
display: flex;
align-items: center;
justify-content: space-between;
.lc-field-title {
display: flex;
align-items: center;
}
.lc-field-icon {
margin-right: @x-gap;
transform-origin: center;
transition: transform 0.1s;
}
}
&.lc-plain-field {
// for top-level style
padding: 8px 10px;
> .lc-field-body {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
}
&.lc-inline-field {
display: flex;
align-items: center;
// for top-level style
padding: 8px 10px;
> .lc-field-head {
width: 70px;
margin-right: 1px;
.lc-title-label {
width: 70px;
word-break: break-all;
}
}
> .lc-field-body {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
}
}
&.lc-block-field, &.lc-accordion-field {
display: block;
&:not(:first-child) {
border-top: 1px solid var(--color-line-normal, @dark-alpha-2);
}
> .lc-field-head {
padding-left: @x-gap;
height: 32px;
display: flex;
align-items: center;
font-weight: 500;
background: var(--color-block-background-shallow, rgba(31,56,88,.06));
border-bottom: 1px solid var(--color-line-normal, @dark-alpha-2);
color: var(--color-title, @white-alpha-2);
user-select: none;
}
> .lc-field-body {
padding: @y-gap @x-gap/2;
}
+ .lc-inline-field {
border-top: 1px solid var(--color-line-normal, @dark-alpha-2);
}
}
.lc-setter-actions {
display: flex;
align-items: center;
}
&.lc-block-field {
position: relative;
>.lc-field-body>.lc-block-setter>.lc-setter-actions {
position: absolute;
right: 10px;
top: 0;
height: 32px;
display: flex;
align-items: center;
}
}
&.lc-accordion-field {
// collapsed
&.lc-field-is-collapsed {
> .lc-field-head .lc-field-icon {
transform: rotate(180deg);
}
> .lc-field-body {
display: none;
}
}
// 邻近的保持上下距离
+ .lc-field {
margin-top: @y-gap;
}
}
// 2rd level reset
.lc-field-body {
.lc-inline-field {
padding: @y-gap @x-gap/2 0 @x-gap/2;
&:first-child {
padding-top: 0;
}
+ .lc-accordion-field, +.lc-block-field {
margin-top: @y-gap;
}
}
.lc-field {
border-top: none !important;
}
.lc-accordion-field, .lc-block-field {
> .lc-field-head {
padding-left: @x-gap/2;
background: var(--color-block-background-light);
border-bottom-color: var(--color-line-light);
> .lc-field-icon {
margin-right: @x-gap/2;
}
}
}
// 3rd level field title width should short
.lc-field-body .lc-inline-field {
> .lc-field-head {
width: 50px;
.lc-title-label {
width: 50px;
}
}
}
}
}

View File

@ -0,0 +1,28 @@
import { ReactNode, createElement } from 'react';
import { TitleContent } from '@ali/lowcode-types';
import './index.less';
import { Field, PopupField, EntryField, PlainField } from './fields';
export interface FieldProps {
className?: string;
title?: TitleContent | null;
display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry';
collapsed?: boolean;
onExpandChange?: (collapsed: boolean) => void;
[extra: string]: any;
}
export function createField(props: FieldProps, children: ReactNode, type?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry') {
if (type === 'popup') {
return createElement(PopupField, props, children);
}
if (type === 'entry') {
return createElement(EntryField, props, children);
}
if (type === 'plain' || !props.title) {
return createElement(PlainField, props, children);
}
return createElement(Field, { ...props, defaultDisplay: type }, children);
}
export { Field, PopupField, EntryField, PlainField };

View File

@ -0,0 +1,262 @@
import React, { Component, isValidElement } from 'react';
import classNames from 'classnames';
import { Dropdown, Menu } from '@alifd/next';
import {
SetterConfig,
CustomView,
DynamicProps,
DynamicSetter,
TitleContent,
isSetterConfig,
isDynamicSetter,
isI18nData,
} from '@ali/lowcode-types';
import {
getSetter,
getSettersMap,
computed,
obx,
Title,
createSetterContent,
observer,
shallowIntl,
Tip,
} from '@ali/lowcode-editor-core';
import { IconConvert } from '../../icons/convert';
import './style.less';
import { SettingField } from '@ali/lowcode-designer';
export interface SetterItem {
name: string;
title: TitleContent;
setter: string | DynamicSetter | CustomView;
props?: object | DynamicProps;
condition?: (field: SettingField) => boolean;
initialValue?: any | ((field: SettingField) => any);
list: boolean;
}
function nomalizeSetters(setters?: Array<string | SetterConfig | CustomView | DynamicSetter>): SetterItem[] {
if (!setters) {
const normalized: SetterItem[] = [];
getSettersMap().forEach((setter, name) => {
if (name === 'MixedSetter') {
return;
}
normalized.push({
name,
title: setter.title || name,
setter: name,
condition: setter.condition,
initialValue: setter.initialValue,
list: setter.recommend || false,
});
});
return normalized;
}
const names: string[] = [];
function generateName(n: string) {
let idx = 1;
let got = n;
while (names.indexOf(got) > -1) {
got = `${n}:${idx++}`;
}
names.push(got);
return got;
}
return setters.map((setter) => {
const config: any = {
setter,
list: true,
};
if (isSetterConfig(setter)) {
config.setter = setter.componentName;
config.props = setter.props;
config.condition = setter.condition;
config.initialValue = setter.initialValue;
config.title = setter.title;
}
if (typeof config.setter === 'string') {
config.name = config.setter;
names.push(config.name);
const info = getSetter(config.setter);
if (!config.title) {
config.title = info?.title || config.setter;
}
if (!config.condition) {
config.condition = info?.condition;
}
if (!config.initialValue) {
config.initialValue = info?.initialValue;
}
} else {
config.name = generateName((config.setter as any).displayName || (config.setter as any).name || 'CustomSetter');
if (!config.title) {
config.title = config.name;
}
}
return config;
});
}
@observer
export default class MixedSetter extends Component<{
field: SettingField;
setters?: Array<string | SetterConfig | CustomView | DynamicSetter>;
onSetterChange?: (field: SettingField, name: string) => void;
onChange?: (val: any) => void;
value?: any;
className?: string;
}> {
private setters = nomalizeSetters(this.props.setters);
@obx.ref private used?: string;
@computed private getCurrentSetter() {
const { field } = this.props;
let firstMatched: SetterItem | undefined;
for (const setter of this.setters) {
const matched = !setter.condition || setter.condition(field);
if (matched) {
if (setter.name === this.used) {
return setter;
}
if (!firstMatched) {
firstMatched = setter;
}
}
}
return firstMatched;
}
private useSetter = (name: string) => {
if (name === this.used) {
return;
}
const { field, onChange } = this.props;
const setter = this.setters.find((item) => item.name === name);
this.used = name;
if (setter) {
let newValue: any = setter.initialValue;
if (newValue && typeof newValue === 'function') {
newValue = newValue(field);
}
onChange && onChange(newValue);
}
};
private shell: HTMLDivElement | null = null;
private checkIsBlockField() {
if (this.shell) {
const setter = this.shell.firstElementChild;
if (setter && setter.classList.contains('lc-block-setter')) {
this.shell.classList.add('lc-block-setter');
} else {
this.shell.classList.remove('lc-block-setter');
}
}
}
componentDidUpdate() {
this.checkIsBlockField();
}
componentDidMount() {
this.checkIsBlockField();
}
render() {
const { className, field, setters, onSetterChange, ...restProps } = this.props;
const currentSetter = this.getCurrentSetter();
const isTwoType = this.setters.length < 3;
let setterContent: any;
const triggerTitle: any = {
tip: {
type: 'i18n',
'zh-CN': '切换格式',
'en-US': 'Switch Format',
},
icon: <IconConvert size={24} />,
};
if (currentSetter) {
const { setter, title, props } = currentSetter;
let setterProps: any = {};
let setterType: any;
if (isDynamicSetter(setter)) {
setterType = setter.call(field, field);
} else {
setterType = setter;
}
if (props) {
setterProps = props;
if (typeof setterProps === 'function') {
setterProps = setterProps(field);
}
}
setterContent = createSetterContent(setterType, {
...shallowIntl(setterProps),
field,
...restProps,
});
if (title) {
if (typeof title !== 'object' || isI18nData(title) || isValidElement(title)) {
triggerTitle.tip = title;
} else {
triggerTitle.tip = title.tip || title.label;
}
}
} else {
// 未匹配的 null 值,显示 NullValue 空值
// 未匹配的 其它 值,显示 InvalidValue 非法值
if (restProps.value == null) {
setterContent = <span>NullValue</span>;
} else {
setterContent = <span>InvalidValue</span>;
}
}
const usedName = currentSetter?.name || this.used;
let moreBtnNode = (
<Title
title={triggerTitle}
className="lc-switch-trigger"
onClick={
isTwoType
? () => {
if (this.setters[0]?.name === usedName) {
this.useSetter(this.setters[1]?.name);
} else {
this.useSetter(this.setters[0]?.name);
}
}
: undefined
}
/>
);
if (!isTwoType) {
moreBtnNode = (
<Dropdown trigger={moreBtnNode} triggerType="click" align="tr br">
<Menu selectMode="single" hasSelectedIcon={true} selectedKeys={usedName} onItemClick={this.useSetter}>
{this.setters
.filter((setter) => setter.list || setter.name === usedName)
.map((setter) => {
return (
<Menu.Item key={setter.name}>
<Title title={setter.title} />
</Menu.Item>
);
})}
</Menu>
</Dropdown>
);
}
return (
<div ref={(shell) => (this.shell = shell)} className={classNames('lc-setter-mixed', className)}>
{setterContent}
<div className="lc-setter-actions">{moreBtnNode}</div>
</div>
);
}
}

View File

@ -0,0 +1,30 @@
.lc-setter-mixed {
flex: 1;
min-width: 0;
margin-right: 26px;
display: block;
position: relative;
>.lc-setter-actions {
position: absolute;
right: -2px;
top: 50%;
transform: translate(100%, -50%);
.lc-switch-trigger {
cursor: pointer;
opacity: 0.6;
&:hover {
opacity: 1;
}
}
}
.next-input,.next-date-picker {
width: 100%;
}
&.lc-block-setter {
position: static;
margin-right: 0;
>.lc-setter-actions {
transform: none;
}
}
}

View File

@ -0,0 +1,181 @@
import { Component, Fragment } from 'react';
import { Icon, Button } from '@alifd/next';
import { SetterType, FieldConfig } from '@ali/lowcode-types';
import { createSettingFieldView } from '../settings/settings-pane';
import { PopupContext, PopupPipe } from '../popup';
import { SettingField } from '@ali/lowcode-designer';
import './style.less';
import { Title } from '@ali/lowcode-editor-core';
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 {...props} />;
}
}
}
interface ObjectSetterConfig {
items?: FieldConfig[];
extraSetter?: SetterType;
}
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.isRequired || conf.important || (conf.setter as any)?.isRequired) {
const item = field.createField({
...conf,
// in column-cell
forceInline: 3,
});
items.push(item);
}
}
}
if (items.length > 0) {
this.items = items;
}
let firstRun: boolean = true;
field.onEffect(() => {
let state: any = {};
if (descriptor) {
if (typeof descriptor === 'function') {
state.descriptor = descriptor(field);
} else {
state.descriptor = field.getPropValue(descriptor);
}
} else {
state.descriptor = field.title;
}
if (firstRun) {
firstRun = false;
this.state = state;
} else {
this.setState(state);
}
});
}
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, config } = 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} field={field} config={config} />, 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, field.id);
}}
>
<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, field.id);
}}
>
<Icon type="edit" />
{title}
</Button>
);
}
}
interface FormSetterProps {
field: SettingField;
config: ObjectSetterConfig;
}
class FormSetter extends Component<FormSetterProps> {
private items: SettingField[];
constructor(props: RowSetterProps) {
super(props);
const { config, field } = props;
this.items = (config.items || []).map((conf) => field.createField(conf));
// TODO: extraConfig for custom fields
}
shouldComponentUpdate() {
return false;
}
render() {
const { field } = this.props;
return (
<div className="lc-setter-object lc-block-setter">
{this.items.map((item, index) => createSettingFieldView(item, field, index))}
</div>
);
}
}

View File

@ -0,0 +1,31 @@
.lc-setter-object-row {
display: flex;
align-items: stretch;
width: 100%;
.lc-setter-object-row-edit {
width: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.lc-setter-object-row-body {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
.lc-field {
padding: 0 !important;
.lc-field-body {
padding: 0 !important; margin: 0 !important;
}
}
> * {
flex: 1;
flex-shrink: 1;
margin-left: 2px;
min-width: 0;
overflow: hidden;
}
}
}

View File

@ -0,0 +1,150 @@
import { createContext, ReactNode, Component, PureComponent } from 'react';
import { EventEmitter } from 'events';
import { Balloon } from '@alifd/next';
import { uniqueId } from '@ali/lowcode-utils';
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, actionKey?: string) => {
this.currentId = id;
this.popup(
{
...props,
actionKey,
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<{ actionKey?: string; safeId?: string }> {
private popupPipe = new PopupPipe();
componentWillUnmount() {
this.popupPipe.purge();
}
render() {
const { children, actionKey, safeId } = this.props;
return (
<PopupContext.Provider value={this.popupPipe}>
{children}
<PopupContent key={'pop' + actionKey} safeId={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, actionKey } = 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}
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 actionKey={actionKey} safeId={id}>
{content}
</PopupService>
</div>
</Balloon>
);
}
}

Some files were not shown because too many files have changed in this diff Show More