mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-13 04:03:07 +00:00
prepare outline
This commit is contained in:
parent
f0a023df6d
commit
dc8fdae09d
@ -1 +1,4 @@
|
||||
编排模块
|
||||
|
||||
|
||||
simulator/renderer 发 CDN
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "lowcode-designer",
|
||||
"name": "@ali/lowcode-designer",
|
||||
"version": "0.9.0",
|
||||
"description": "alibaba lowcode designer",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Component } from 'react';
|
||||
import { obx } from '@recore/obx';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { observer, obx } from '../../../../globals';
|
||||
import Designer from '../../designer/designer';
|
||||
import { isDragNodeObject, DragObject, isDragNodeDataObject } from '../../designer/helper/dragon';
|
||||
import './ghost.less';
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from './auxiliary';
|
||||
@ -1,4 +1,4 @@
|
||||
.lc-auxiliary {
|
||||
.lc-bem-tools {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -1,12 +1,11 @@
|
||||
import { Component, Fragment, PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { SimulatorHost } from '../host';
|
||||
import { computed } from '@recore/obx';
|
||||
import { computed, observer, TitleContent, Title } from '../../../../../../globals/src';
|
||||
|
||||
export class OutlineHoveringInstance extends PureComponent<{
|
||||
title: string;
|
||||
export class BorderHoveringInstance extends PureComponent<{
|
||||
title: TitleContent;
|
||||
rect: DOMRect | null;
|
||||
scale: number;
|
||||
scrollX: number;
|
||||
@ -24,7 +23,7 @@ export class OutlineHoveringInstance extends PureComponent<{
|
||||
transform: `translate(${(scrollX + rect.left) * scale}px, ${(scrollY + rect.top) * scale}px)`,
|
||||
};
|
||||
|
||||
const className = classNames('lc-outlines lc-outlines-hovering');
|
||||
const className = classNames('lc-borders lc-borders-hovering');
|
||||
|
||||
// TODO:
|
||||
// 1. thinkof icon
|
||||
@ -32,14 +31,14 @@ export class OutlineHoveringInstance extends PureComponent<{
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<a className="lc-outlines-title">{title}</a>
|
||||
<Title title={title} className="lc-borders-title" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class OutlineHovering extends Component {
|
||||
export class BorderHovering extends Component {
|
||||
static contextType = SimulatorContext;
|
||||
|
||||
shouldComponentUpdate() {
|
||||
@ -82,7 +81,7 @@ export class OutlineHovering extends Component {
|
||||
|
||||
if (instances.length === 1) {
|
||||
return (
|
||||
<OutlineHoveringInstance
|
||||
<BorderHoveringInstance
|
||||
key="line-h"
|
||||
title={current.title}
|
||||
scale={this.scale}
|
||||
@ -95,7 +94,7 @@ export class OutlineHovering extends Component {
|
||||
return (
|
||||
<Fragment>
|
||||
{instances.map((inst, i) => (
|
||||
<OutlineHoveringInstance
|
||||
<BorderHoveringInstance
|
||||
key={`line-h-${i}`}
|
||||
title={current.title}
|
||||
scale={this.scale}
|
||||
@ -9,17 +9,22 @@ import {
|
||||
ComponentType,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { SimulatorHost } from '../host';
|
||||
import { computed } from '@recore/obx';
|
||||
import OffsetObserver from '../../../../designer/helper/offset-observer';
|
||||
import Node from '../../../../designer/document/node/node';
|
||||
import { isContentObject, ContentObject } from '../../../../designer/component-meta';
|
||||
import { createIcon, EmbedTip, isReactComponent } from '../../../../../../globals';
|
||||
import {
|
||||
observer,
|
||||
computed,
|
||||
createIcon,
|
||||
EmbedTip,
|
||||
isReactComponent,
|
||||
ActionContentObject,
|
||||
isActionContentObject,
|
||||
} from '../../../../../../globals';
|
||||
|
||||
@observer
|
||||
export class OutlineSelectingInstance extends Component<{
|
||||
export class BorderSelectingInstance extends Component<{
|
||||
observed: OffsetObserver;
|
||||
highlight?: boolean;
|
||||
dragging?: boolean;
|
||||
@ -42,7 +47,7 @@ export class OutlineSelectingInstance extends Component<{
|
||||
transform: `translate3d(${offsetLeft}px, ${offsetTop}px, 0)`,
|
||||
};
|
||||
|
||||
const className = classNames('lc-outlines lc-outlines-selecting', {
|
||||
const className = classNames('lc-borders lc-borders-selecting', {
|
||||
highlight,
|
||||
dragging,
|
||||
});
|
||||
@ -100,26 +105,26 @@ class Toolbar extends Component<{ observed: OffsetObserver }> {
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="lc-outlines-actions" style={style}>
|
||||
<div className="lc-borders-actions" style={style}>
|
||||
{actions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createAction(content: ReactNode | ComponentType<any> | ContentObject, key: string, node: Node) {
|
||||
function createAction(content: ReactNode | ComponentType<any> | ActionContentObject, key: string, node: Node) {
|
||||
if (isValidElement(content)) {
|
||||
return cloneElement(content, { key, node });
|
||||
}
|
||||
if (isReactComponent(content)) {
|
||||
return createElement(content, { key, node });
|
||||
}
|
||||
if (isContentObject(content)) {
|
||||
if (isActionContentObject(content)) {
|
||||
const { action, description, icon } = content;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="lc-outlines-action"
|
||||
className="lc-borders-action"
|
||||
onClick={() => {
|
||||
action && action(node);
|
||||
}}
|
||||
@ -133,7 +138,7 @@ function createAction(content: ReactNode | ComponentType<any> | ContentObject, k
|
||||
}
|
||||
|
||||
@observer
|
||||
export class OutlineSelectingForNode extends Component<{ node: Node }> {
|
||||
export class BorderSelectingForNode extends Component<{ node: Node }> {
|
||||
static contextType = SimulatorContext;
|
||||
|
||||
get host(): SimulatorHost {
|
||||
@ -170,7 +175,7 @@ export class OutlineSelectingForNode extends Component<{ node: Node }> {
|
||||
if (!observed) {
|
||||
return null;
|
||||
}
|
||||
return <OutlineSelectingInstance key={observed.id} dragging={this.dragging} observed={observed} />;
|
||||
return <BorderSelectingInstance key={observed.id} dragging={this.dragging} observed={observed} />;
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
@ -178,7 +183,7 @@ export class OutlineSelectingForNode extends Component<{ node: Node }> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class OutlineSelecting extends Component {
|
||||
export class BorderSelecting extends Component {
|
||||
static contextType = SimulatorContext;
|
||||
|
||||
get host(): SimulatorHost {
|
||||
@ -212,7 +217,7 @@ export class OutlineSelecting extends Component {
|
||||
return (
|
||||
<Fragment>
|
||||
{selecting.map(node => (
|
||||
<OutlineSelectingForNode key={node.id} node={node} />
|
||||
<BorderSelectingForNode key={node.id} node={node} />
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
@ -1,4 +1,4 @@
|
||||
@scope: lc-outlines;
|
||||
@scope: lc-borders;
|
||||
|
||||
.@{scope} {
|
||||
box-sizing: border-box;
|
||||
@ -1,15 +1,15 @@
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { Component } from 'react';
|
||||
import { OutlineHovering } from './outline-hovering';
|
||||
import { observer } from '../../../../../../globals';
|
||||
import { BorderHovering } from './border-hovering';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { SimulatorHost } from '../host';
|
||||
import { OutlineSelecting } from './outline-selecting';
|
||||
import { BorderSelecting } from './border-selecting';
|
||||
import { InsertionView } from './insertion';
|
||||
import './auxiliary.less';
|
||||
import './outlines.less';
|
||||
import './bem-tools.less';
|
||||
import './borders.less';
|
||||
|
||||
@observer
|
||||
export class AuxiliaryView extends Component {
|
||||
export class BemTools extends Component {
|
||||
static contextType = SimulatorContext;
|
||||
|
||||
shouldComponentUpdate() {
|
||||
@ -20,9 +20,9 @@ export class AuxiliaryView extends Component {
|
||||
const host = this.context as SimulatorHost;
|
||||
const { scrollX, scrollY, scale } = host.viewport;
|
||||
return (
|
||||
<div className="lc-auxiliary" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
|
||||
<OutlineHovering key="hovering" />
|
||||
<OutlineSelecting key="selecting" />
|
||||
<div className="lc-bem-tools" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
|
||||
<BorderHovering key="hovering" />
|
||||
<BorderSelecting key="selecting" />
|
||||
<InsertionView key="insertion" />
|
||||
</div>
|
||||
);
|
||||
@ -1,6 +1,5 @@
|
||||
import { Component } from 'react';
|
||||
import { computed } from '@recore/obx';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { computed, observer } from '../../../../../../globals';
|
||||
import { SimulatorContext } from '../context';
|
||||
import { SimulatorHost } from '../host';
|
||||
import Location, {
|
||||
@ -1,9 +1,9 @@
|
||||
import { Component } from 'react';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { observer } from '../../../../../globals';
|
||||
import { SimulatorHost, SimulatorProps } from './host';
|
||||
import DocumentModel from '../../../designer/document/document-model';
|
||||
import { SimulatorContext } from './context';
|
||||
import { AuxiliaryView } from './auxilary';
|
||||
import { BemTools } from './bem-tools';
|
||||
import './host.less';
|
||||
|
||||
/*
|
||||
@ -65,7 +65,7 @@ class Canvas extends Component {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div ref={elmt => sim.mountViewport(elmt)} className="lc-simulator-canvas-viewport">
|
||||
<AuxiliaryView />
|
||||
<BemTools />
|
||||
<Content />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { obx, autorun, computed } from '@recore/obx';
|
||||
import { obx, autorun, computed } from '../../../../../globals';
|
||||
import { ISimulator, Component, NodeInstance } from '../../../designer/simulator';
|
||||
import Viewport from './viewport';
|
||||
import { createSimulator } from './create-simulator';
|
||||
@ -28,12 +28,11 @@ import {
|
||||
Rect,
|
||||
CanvasPoint,
|
||||
} from '../../../designer/helper/location';
|
||||
import { isNodeSchema, NodeSchema } from '../../../designer/schema';
|
||||
import { ComponentMetadata } from '../../../designer/component-meta';
|
||||
import { ReactInstance } from 'react';
|
||||
import { isRootNode } from '../../../designer/document/node/root-node';
|
||||
import { parseProps } from '../utils/parse-props';
|
||||
import { isElement } from '../../../utils/is-element';
|
||||
import { ComponentMetadata, NodeSchema, isNodeSchema } from '../../../../../globals';
|
||||
|
||||
export interface LibraryItem {
|
||||
package: string;
|
||||
@ -464,24 +463,17 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
return null;
|
||||
}
|
||||
|
||||
let elems = elements.slice();
|
||||
if (selector) {
|
||||
const matched = getMatched(elems, selector);
|
||||
if (!matched) {
|
||||
return null;
|
||||
}
|
||||
elems = [matched];
|
||||
}
|
||||
let rects: DOMRect[] | undefined;
|
||||
let last: { x: number; y: number; r: number; b: number } | undefined;
|
||||
let computed = false;
|
||||
const elems = selector
|
||||
? elements
|
||||
.map(elem => {
|
||||
if (isElement(elem)) {
|
||||
// TODO: if has selector use exact match
|
||||
if (elem.matches(selector)) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
return elem.querySelector(selector);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
: elements.slice();
|
||||
while (true) {
|
||||
if (!rects || rects.length < 1) {
|
||||
const elem = elems.pop();
|
||||
@ -1057,3 +1049,19 @@ function isNearAfter(point: CanvasPoint, rect: Rect, inline: boolean) {
|
||||
}
|
||||
return Math.abs(point.canvasY - rect.top) > Math.abs(point.canvasY - rect.bottom);
|
||||
}
|
||||
|
||||
function getMatched(elements: Array<Element | Text>, selector: string): Element | null {
|
||||
let firstQueried: Element | null = null;
|
||||
for (const elem of elements) {
|
||||
if (isElement(elem)) {
|
||||
if (elem.matches(selector)) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
if (!firstQueried) {
|
||||
firstQueried = elem.querySelector(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
return firstQueried;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { SimulatorRenderer } from '../renderer/renderer';
|
||||
import { autorun, obx } from '@recore/obx';
|
||||
import { autorun, obx } from '../../../../../globals';
|
||||
import { SimulatorHost } from './host';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
@ -34,7 +34,6 @@ export default class ResourceConsumer<T = any> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _consuming?: () => void;
|
||||
consume(consumerOrRenderer: SimulatorRenderer | ((data: T) => any)) {
|
||||
if (this._consuming) {
|
||||
@ -48,7 +47,7 @@ export default class ResourceConsumer<T = any> {
|
||||
}
|
||||
const rendererConsumer = this.consumer!;
|
||||
|
||||
consumer = (data) => rendererConsumer(consumerOrRenderer, data);
|
||||
consumer = data => rendererConsumer(consumerOrRenderer, data);
|
||||
} else {
|
||||
consumer = consumerOrRenderer;
|
||||
}
|
||||
@ -76,14 +75,14 @@ export default class ResourceConsumer<T = any> {
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
private _firstConsumed: boolean = false;
|
||||
private _firstConsumed = false;
|
||||
private resovleFirst?: () => void;
|
||||
|
||||
waitFirstConsume(): Promise<any> {
|
||||
if (this._firstConsumed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.resovleFirst = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { obx, computed } from '../../../../../globals';
|
||||
import { Point } from '../../../designer/helper/location';
|
||||
import { ScrollTarget } from '../../../designer/helper/scroller';
|
||||
import { AutoFit, IViewport } from '../../../designer/simulator';
|
||||
|
||||
@ -3,7 +3,6 @@ import { render as reactRender } from 'react-dom';
|
||||
import { host } from './host';
|
||||
import SimulatorRendererView from './renderer-view';
|
||||
import { computed, obx } from '@recore/obx';
|
||||
import { RootSchema, NpmInfo } from '../../../designer/schema';
|
||||
import { getClientRects } from '../../../utils/get-client-rects';
|
||||
import { Asset } from '../utils/asset';
|
||||
import loader from '../utils/loader';
|
||||
@ -13,6 +12,7 @@ import { NodeInstance } from '../../../designer/simulator';
|
||||
import { isElement } from '../../../utils/is-element';
|
||||
import cursor from '../../../designer/helper/cursor';
|
||||
import { setNativeSelection } from '../../../designer/helper/navtive-selection';
|
||||
import { RootSchema, NpmInfo } from '../../../../../globals/src';
|
||||
|
||||
export class SimulatorRenderer {
|
||||
readonly isSimulatorRenderer = true;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { isValidElement } from 'react';
|
||||
import { isElement } from '../../../utils/is-element';
|
||||
import { PropType, PropConfig } from '../../../designer/prop-config';
|
||||
import { PropConfig } from '../../../../../globals';
|
||||
|
||||
export const primitiveTypes = [
|
||||
'string',
|
||||
|
||||
@ -1,96 +1,24 @@
|
||||
import { ReactNode, ReactElement, ComponentType, createElement } from 'react';
|
||||
import Node, { NodeParent } from './document/node/node';
|
||||
import { NodeData, NodeSchema } from './schema';
|
||||
import { PropConfig } from './prop-config';
|
||||
import Designer from './designer';
|
||||
import { Remove, Clone } from '../../../globals';
|
||||
import { computed } from '@recore/obx';
|
||||
import {
|
||||
IconRemove,
|
||||
IconClone,
|
||||
IconPage,
|
||||
IconContainer,
|
||||
IconComponent,
|
||||
ComponentMetadata,
|
||||
NpmInfo,
|
||||
NodeData,
|
||||
NodeSchema,
|
||||
ComponentAction,
|
||||
TitleContent,
|
||||
TransformedComponentMetadata,
|
||||
getRegisteredMetadataTransducers,
|
||||
registerMetadataTransducer,
|
||||
computed,
|
||||
} from '../../../globals';
|
||||
import { intl } from '../locale';
|
||||
|
||||
export interface NestingRule {
|
||||
childWhitelist?: string[];
|
||||
parentWhitelist?: string[];
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
props?: any[];
|
||||
styles?: object;
|
||||
events?: object;
|
||||
component?: {
|
||||
isContainer?: boolean;
|
||||
isModal?: boolean;
|
||||
descriptor?: string;
|
||||
nestingRule?: NestingRule;
|
||||
rectSelector?: string;
|
||||
// copy,move,delete
|
||||
disableBehaviors?: string[];
|
||||
actions?: ComponentAction[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ContentObject {
|
||||
// 图标
|
||||
icon?: string | ComponentType<any> | ReactElement;
|
||||
// 描述
|
||||
description?: string;
|
||||
// 执行动作
|
||||
action?: (node: Node) => void;
|
||||
}
|
||||
|
||||
export interface ComponentAction {
|
||||
// behaviorName
|
||||
name: string;
|
||||
// 菜单名称
|
||||
content: string | ReactNode | ContentObject;
|
||||
// 子集
|
||||
items?: ComponentAction[];
|
||||
// 不显示
|
||||
condition?: boolean | ((node: Node) => boolean);
|
||||
// 显示在工具条上
|
||||
important?: boolean;
|
||||
}
|
||||
|
||||
export function isContentObject(obj: any): obj is ContentObject {
|
||||
return obj && typeof obj === 'object';
|
||||
}
|
||||
|
||||
export interface ComponentMetadata {
|
||||
componentName: string;
|
||||
/**
|
||||
* unique id
|
||||
*/
|
||||
uri?: string;
|
||||
/**
|
||||
* title or description
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* svg icon for component
|
||||
*/
|
||||
icon?: string | ReactNode;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
docUrl?: string;
|
||||
screenshot?: string;
|
||||
devMode?: 'procode' | 'lowcode';
|
||||
npm?: {
|
||||
package: string;
|
||||
exportName: string;
|
||||
subName: string;
|
||||
main: string;
|
||||
destructuring: boolean;
|
||||
version: string;
|
||||
};
|
||||
props?: PropConfig[];
|
||||
configure?: any[] | Configure;
|
||||
}
|
||||
|
||||
interface TransformedComponentMetadata extends ComponentMetadata {
|
||||
configure: Configure & {
|
||||
combined?: any[];
|
||||
};
|
||||
}
|
||||
|
||||
function ensureAList(list?: string | string[]): string[] | null {
|
||||
if (!list) {
|
||||
return null;
|
||||
@ -136,22 +64,16 @@ function npmToURI(npm: {
|
||||
return uri;
|
||||
}
|
||||
|
||||
export type MetadataTransducer = (prev: TransformedComponentMetadata) => TransformedComponentMetadata;
|
||||
const metadataTransducers: MetadataTransducer[] = [];
|
||||
|
||||
// propsParser
|
||||
//
|
||||
|
||||
export function registerMetadataTransducer(transducer: MetadataTransducer) {
|
||||
metadataTransducers.push(transducer);
|
||||
}
|
||||
|
||||
export class ComponentMeta {
|
||||
readonly isComponentMeta = true;
|
||||
private _uri?: string;
|
||||
get uri(): string {
|
||||
return this._uri!;
|
||||
}
|
||||
private _npm?: NpmInfo;
|
||||
get npm() {
|
||||
return this._npm;
|
||||
}
|
||||
private _componentName?: string;
|
||||
get componentName(): string {
|
||||
return this._componentName!;
|
||||
@ -172,10 +94,6 @@ export class ComponentMeta {
|
||||
get rectSelector(): string | undefined {
|
||||
return this._rectSelector;
|
||||
}
|
||||
private _acceptable?: boolean;
|
||||
get acceptable(): boolean {
|
||||
return this._acceptable!;
|
||||
}
|
||||
private _transformedMetadata?: TransformedComponentMetadata;
|
||||
get configure() {
|
||||
const config = this._transformedMetadata?.configure;
|
||||
@ -185,20 +103,30 @@ export class ComponentMeta {
|
||||
private parentWhitelist?: string[] | null;
|
||||
private childWhitelist?: string[] | null;
|
||||
|
||||
private _title?: TitleContent;
|
||||
get title() {
|
||||
return this._metadata.title || this.componentName;
|
||||
return this._title || this.componentName;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._metadata.icon;
|
||||
return (
|
||||
this._transformedMetadata?.icon ||
|
||||
(this.componentName === 'Page' ? IconPage : this.isContainer ? IconContainer : IconComponent)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(readonly designer: Designer, private _metadata: ComponentMetadata) {
|
||||
this.parseMetadata(_metadata);
|
||||
private _acceptable?: boolean;
|
||||
get acceptable(): boolean {
|
||||
return this._acceptable!;
|
||||
}
|
||||
|
||||
constructor(readonly designer: Designer, metadata: ComponentMetadata) {
|
||||
this.parseMetadata(metadata);
|
||||
}
|
||||
|
||||
private parseMetadata(metadta: ComponentMetadata) {
|
||||
const { componentName, uri, npm, props } = metadta;
|
||||
const { componentName, uri, npm } = metadta;
|
||||
this._npm = npm;
|
||||
this._uri = uri || (npm ? npmToURI(npm) : componentName);
|
||||
this._componentName = componentName;
|
||||
|
||||
@ -206,6 +134,15 @@ export class ComponentMeta {
|
||||
// 额外转换逻辑
|
||||
this._transformedMetadata = this.transformMetadata(metadta);
|
||||
|
||||
const title = this._transformedMetadata.title;
|
||||
if (title && typeof title === 'string') {
|
||||
this._title = {
|
||||
type: 'i18n',
|
||||
'en-US': this.componentName,
|
||||
'zh-CN': title,
|
||||
};
|
||||
}
|
||||
|
||||
const { configure = {} } = this._transformedMetadata;
|
||||
this._acceptable = false;
|
||||
|
||||
@ -227,7 +164,7 @@ export class ComponentMeta {
|
||||
}
|
||||
|
||||
private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata {
|
||||
const result = metadataTransducers.reduce((prevMetadata, current) => {
|
||||
const result = getRegisteredMetadataTransducers().reduce((prevMetadata, current) => {
|
||||
return current(prevMetadata);
|
||||
}, preprocessMetadata(metadta));
|
||||
|
||||
@ -253,13 +190,12 @@ export class ComponentMeta {
|
||||
return actions;
|
||||
}
|
||||
|
||||
set metadata(metadata: ComponentMetadata) {
|
||||
this._metadata = metadata;
|
||||
setMetadata(metadata: ComponentMetadata) {
|
||||
this.parseMetadata(metadata);
|
||||
}
|
||||
|
||||
get metadata(): ComponentMetadata {
|
||||
return this._metadata;
|
||||
getMetadata(): TransformedComponentMetadata {
|
||||
return this._transformedMetadata!;
|
||||
}
|
||||
|
||||
checkNestingUp(my: Node | NodeData, parent: NodeParent) {
|
||||
@ -340,7 +276,7 @@ const builtinComponentActions: ComponentAction[] = [
|
||||
{
|
||||
name: 'remove',
|
||||
content: {
|
||||
icon: Remove,
|
||||
icon: IconRemove,
|
||||
description: intl('remove'),
|
||||
action(node: Node) {
|
||||
node.remove();
|
||||
@ -351,7 +287,7 @@ const builtinComponentActions: ComponentAction[] = [
|
||||
{
|
||||
name: 'copy',
|
||||
content: {
|
||||
icon: Clone,
|
||||
icon: IconClone,
|
||||
description: intl('copy'),
|
||||
action(node: Node) {
|
||||
// node.remove();
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { ComponentType as ReactComponentType } from 'react';
|
||||
import { obx, computed, autorun } from '@recore/obx';
|
||||
import { ComponentType } from 'react';
|
||||
import BuiltinSimulatorView from '../builtins/simulator';
|
||||
import Project from './project';
|
||||
import { ProjectSchema, NpmInfo } from './schema';
|
||||
import Dragon, { isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './helper/dragon';
|
||||
import ActiveTracker from './helper/active-tracker';
|
||||
import Hovering from './helper/hovering';
|
||||
@ -10,11 +8,12 @@ import Location, { LocationData, isLocationChildrenDetail } from './helper/locat
|
||||
import DocumentModel from './document/document-model';
|
||||
import Node, { insertChildren } from './document/node/node';
|
||||
import { isRootNode } from './document/node/root-node';
|
||||
import { ComponentMetadata, ComponentMeta, ComponentAction } from './component-meta';
|
||||
import { ComponentMeta } from './component-meta';
|
||||
import Scroller, { IScrollable } from './helper/scroller';
|
||||
import { INodeSelector } from './simulator';
|
||||
import OffsetObserver, { createOffsetObserver } from './helper/offset-observer';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProjectSchema, ComponentMetadata, ComponentAction, NpmInfo, obx, computed, autorun } from '../../../globals';
|
||||
|
||||
export interface DesignerProps {
|
||||
className?: string;
|
||||
@ -22,8 +21,8 @@ export interface DesignerProps {
|
||||
defaultSchema?: ProjectSchema;
|
||||
hotkeys?: object;
|
||||
simulatorProps?: object | ((document: DocumentModel) => object);
|
||||
simulatorComponent?: ReactComponentType<any>;
|
||||
dragGhostComponent?: ReactComponentType<any>;
|
||||
simulatorComponent?: ComponentType<any>;
|
||||
dragGhostComponent?: ComponentType<any>;
|
||||
suspensed?: boolean;
|
||||
componentMetadatas?: ComponentMetadata[];
|
||||
eventPipe?: EventEmitter;
|
||||
@ -165,6 +164,9 @@ export default class Designer {
|
||||
*/
|
||||
createLocation(locationData: LocationData): Location {
|
||||
const loc = new Location(locationData);
|
||||
if (this._dropLocation && this._dropLocation.document !== loc.document) {
|
||||
this._dropLocation.document.internalSetDropLocation(null);
|
||||
}
|
||||
this._dropLocation = loc;
|
||||
loc.document.internalSetDropLocation(loc);
|
||||
this.activeTracker.track({ node: loc.target, detail: loc.detail });
|
||||
@ -258,9 +260,9 @@ export default class Designer {
|
||||
return this.props ? this.props[key] : null;
|
||||
}
|
||||
|
||||
@obx.ref private _simulatorComponent?: ReactComponentType<any>;
|
||||
@obx.ref private _simulatorComponent?: ComponentType<any>;
|
||||
|
||||
@computed get simulatorComponent(): ReactComponentType<any> {
|
||||
@computed get simulatorComponent(): ComponentType<any> {
|
||||
return this._simulatorComponent || BuiltinSimulatorView;
|
||||
}
|
||||
|
||||
@ -300,12 +302,12 @@ export default class Designer {
|
||||
const key = data.componentName;
|
||||
let meta = this._componentMetasMap.get(key);
|
||||
if (meta) {
|
||||
meta.metadata = data;
|
||||
meta.setMetadata(data);
|
||||
} else {
|
||||
meta = this._lostComponentMetasMap.get(key);
|
||||
|
||||
if (meta) {
|
||||
meta.metadata = data;
|
||||
meta.setMetadata(data);
|
||||
this._lostComponentMetasMap.delete(key);
|
||||
} else {
|
||||
meta = new ComponentMeta(this, data);
|
||||
@ -342,7 +344,9 @@ export default class Designer {
|
||||
@computed get componentsMap(): { [key: string]: NpmInfo } {
|
||||
const maps: any = {};
|
||||
this._componentMetasMap.forEach((config, key) => {
|
||||
maps[key] = config.metadata.npm;
|
||||
if (config.npm) {
|
||||
maps[key] = config.npm;
|
||||
}
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
import Project from '../project';
|
||||
import { RootSchema, NodeData, isDOMText, isJSExpression, NodeSchema } from '../schema';
|
||||
import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './node/node';
|
||||
import { Selection } from './selection';
|
||||
import RootNode from './node/root-node';
|
||||
import { ISimulator, Component } from '../simulator';
|
||||
import { computed, obx, autorun } from '@recore/obx';
|
||||
import { ISimulator } from '../simulator';
|
||||
import Location from '../helper/location';
|
||||
import { ComponentMeta } from '../component-meta';
|
||||
import History from '../helper/history';
|
||||
import Prop from './node/props/prop';
|
||||
import {
|
||||
RootSchema,
|
||||
NodeData,
|
||||
isJSExpression,
|
||||
isDOMText,
|
||||
NodeSchema,
|
||||
computed,
|
||||
obx,
|
||||
autorun,
|
||||
} from '../../../../globals';
|
||||
|
||||
export default class DocumentModel {
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Component } from 'react';
|
||||
import DocumentModel from './document-model';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import { observer } from '../../../../globals';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@observer
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import Node, { NodeParent } from './node';
|
||||
import { NodeData, isNodeSchema } from '../../schema';
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { NodeData, isNodeSchema, obx, computed } from '../../../../../globals';
|
||||
|
||||
export default class NodeChildren {
|
||||
@obx.val private children: Node[];
|
||||
@ -20,7 +19,7 @@ export default class NodeChildren {
|
||||
return this.children.map(node => node.export(serialize));
|
||||
}
|
||||
|
||||
import(data?: NodeData | NodeData[], checkId: boolean = false) {
|
||||
import(data?: NodeData | NodeData[], checkId = false) {
|
||||
data = data ? (Array.isArray(data) ? data : [data]) : [];
|
||||
|
||||
const originChildren = this.children.slice();
|
||||
@ -59,10 +58,14 @@ export default class NodeChildren {
|
||||
return this.size < 1;
|
||||
}
|
||||
|
||||
@computed notEmpty() {
|
||||
return this.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个节点
|
||||
*/
|
||||
delete(node: Node, purge: boolean = false): boolean {
|
||||
delete(node: Node, purge = false): boolean {
|
||||
const i = this.children.indexOf(node);
|
||||
if (i < 0) {
|
||||
return false;
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { NodeSchema, NodeData, PropsMap, PropsList, isDOMText, isJSExpression } from '../../schema';
|
||||
import Props, { EXTRA_KEY_PREFIX } from './props/props';
|
||||
import DocumentModel from '../document-model';
|
||||
import NodeChildren from './node-children';
|
||||
import Prop from './props/prop';
|
||||
import { ComponentMeta } from '../../component-meta';
|
||||
import {
|
||||
isDOMText,
|
||||
isJSExpression,
|
||||
NodeSchema,
|
||||
PropsMap,
|
||||
PropsList,
|
||||
NodeData,
|
||||
TitleContent,
|
||||
obx,
|
||||
computed,
|
||||
} from '../../../../../globals';
|
||||
|
||||
/**
|
||||
* 基础节点
|
||||
@ -74,7 +83,7 @@ export default class Node {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@computed get title(): string {
|
||||
@computed get title(): TitleContent {
|
||||
let t = this.getExtraProp('title');
|
||||
if (!t && this.componentMeta.descriptor) {
|
||||
t = this.getProp(this.componentMeta.descriptor, false);
|
||||
@ -181,6 +190,30 @@ export default class Node {
|
||||
return this.props.export(true).props || null;
|
||||
}
|
||||
|
||||
isContainer() {
|
||||
return this.isNodeParent && this.componentMeta.isContainer;
|
||||
}
|
||||
|
||||
@computed isSlotContainer() {
|
||||
for (const item of this.props) {
|
||||
if (item.type === 'slot') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@computed get slots() {
|
||||
// TODO: optimize recore/obx, array maked every time, donot as changed
|
||||
const slots: Node[] = [];
|
||||
this.props.forEach(item => {
|
||||
if (item.type === 'slot') {
|
||||
slots.push(item.slotNode!);
|
||||
}
|
||||
});
|
||||
return slots;
|
||||
}
|
||||
|
||||
private _conditionGroup: string | null = null;
|
||||
/**
|
||||
* 条件组
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { obx, autorun, untracked, computed } from '@recore/obx';
|
||||
import { obx, autorun, untracked, computed } from '../../../../../../globals';
|
||||
import Prop, { IPropParent, UNSET } from './prop';
|
||||
import Props from './props';
|
||||
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { untracked, computed, obx } from '@recore/obx';
|
||||
import { valueToSource } from '../../../../utils/value-to-source';
|
||||
import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema';
|
||||
import PropStash from './prop-stash';
|
||||
import { uniqueId } from '../../../../../../utils/unique-id';
|
||||
import { isPlainObject } from '../../../../../../utils/is-plain-object';
|
||||
import { hasOwnProperty } from '../../../../utils/has-own-property';
|
||||
import Props from './props';
|
||||
import Node from '../node';
|
||||
import {
|
||||
CompositeValue,
|
||||
isJSExpression,
|
||||
isJSSlot,
|
||||
NodeData,
|
||||
isNodeSchema,
|
||||
untracked,
|
||||
computed,
|
||||
obx,
|
||||
} from '../../../../../../globals';
|
||||
|
||||
export const UNSET = Symbol.for('unset');
|
||||
export type UNSET = typeof UNSET;
|
||||
@ -197,6 +205,9 @@ export default class Prop implements IPropParent {
|
||||
}
|
||||
|
||||
private _slotNode?: Node;
|
||||
get slotNode() {
|
||||
return this._slotNode;
|
||||
}
|
||||
setAsSlot(data: NodeData) {
|
||||
this._type = 'slot';
|
||||
if (
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { computed, obx } from '@recore/obx';
|
||||
import { uniqueId } from '../../../../../../utils/unique-id';
|
||||
import { CompositeValue, PropsList, PropsMap } from '../../../schema';
|
||||
import PropStash from './prop-stash';
|
||||
import Prop, { IPropParent, UNSET } from './prop';
|
||||
import Node from '../node';
|
||||
import { PropsMap, PropsList, CompositeValue, computed, obx } from '../../../../../../globals';
|
||||
|
||||
export const EXTRA_KEY_PREFIX = '__';
|
||||
|
||||
@ -279,6 +278,12 @@ export default class Props implements IPropParent {
|
||||
});
|
||||
}
|
||||
|
||||
filter(fn: (item: Prop, key: number | string | undefined) => boolean) {
|
||||
return this.items.filter(item => {
|
||||
return fn(item, item.key);
|
||||
});
|
||||
}
|
||||
|
||||
private purged = false;
|
||||
/**
|
||||
* 回收销毁
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import Node, { NodeParent } from './node';
|
||||
import { RootSchema } from '../../schema';
|
||||
import DocumentModel from '../document-model';
|
||||
import NodeChildren from './node-children';
|
||||
import Props from './props/props';
|
||||
import { RootSchema } from '../../../../../globals';
|
||||
|
||||
/**
|
||||
* 根容器节点
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Node, { comparePosition, PositionNO } from './node/node';
|
||||
import { obx } from '@recore/obx';
|
||||
import { obx } from '../../../../globals';
|
||||
import DocumentModel from './document-model';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { obx } from '@recore/obx';
|
||||
import Location from './location';
|
||||
import DocumentModel from '../document/document-model';
|
||||
import { NodeSchema } from '../schema';
|
||||
import { ISimulator, isSimulator, ComponentInstance } from '../simulator';
|
||||
import Node from '../document/node/node';
|
||||
import Designer from '../designer';
|
||||
import { setNativeSelection } from './navtive-selection';
|
||||
import cursor from './cursor';
|
||||
import { NodeSchema, obx } from '../../../../globals';
|
||||
|
||||
export interface LocateEvent {
|
||||
readonly type: 'LocateEvent';
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import Session from './session';
|
||||
import { autorun, Reaction, untracked } from '@recore/obx';
|
||||
import { NodeSchema } from '../schema';
|
||||
import { NodeSchema, autorun, Reaction, untracked } from '../../../../globals';
|
||||
|
||||
// TODO: cache to localStorage
|
||||
|
||||
@ -176,3 +174,48 @@ export default class History {
|
||||
this.records = [];
|
||||
}
|
||||
}
|
||||
|
||||
class Session {
|
||||
private _data: any;
|
||||
private activedTimer: any;
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
|
||||
this.setTimer();
|
||||
this.log(data);
|
||||
}
|
||||
|
||||
log(data: any) {
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
this.setTimer();
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.activedTimer != null;
|
||||
}
|
||||
|
||||
end() {
|
||||
if (this.isActive()) {
|
||||
this.clearTimer();
|
||||
console.info('session end');
|
||||
}
|
||||
}
|
||||
|
||||
private setTimer() {
|
||||
this.clearTimer();
|
||||
this.activedTimer = setTimeout(() => this.end(), this.timeGap);
|
||||
}
|
||||
|
||||
private clearTimer() {
|
||||
if (this.activedTimer) {
|
||||
clearTimeout(this.activedTimer);
|
||||
}
|
||||
this.activedTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { obx } from '@recore/obx';
|
||||
import Node from '../document/node/node';
|
||||
import DocumentModel from '../document/document-model';
|
||||
import { obx } from '../../../../globals';
|
||||
|
||||
export default class Hovering {
|
||||
@obx.ref private _enable: boolean = true;
|
||||
@obx.ref private _enable = true;
|
||||
get enable() {
|
||||
return this._enable;
|
||||
}
|
||||
@ -13,7 +13,7 @@ export default class Hovering {
|
||||
this._current = null;
|
||||
}
|
||||
}
|
||||
@obx.ref xRayMode: boolean = false;
|
||||
@obx.ref xRayMode = false;
|
||||
|
||||
@obx.ref private _current: Node | null = null;
|
||||
get current() {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { obx, computed } from '../../../../globals';
|
||||
import { INodeSelector, IViewport } from '../simulator';
|
||||
import { uniqueId } from '../../../../utils/unique-id';
|
||||
import { isRootNode } from '../document/node/root-node';
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
export default class Session {
|
||||
private _data: any;
|
||||
private activedTimer: any;
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
|
||||
this.setTimer();
|
||||
this.log(data);
|
||||
}
|
||||
|
||||
log(data: any) {
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
this.setTimer();
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.activedTimer != null;
|
||||
}
|
||||
|
||||
end() {
|
||||
if (this.isActive()) {
|
||||
this.clearTimer();
|
||||
console.info('session end');
|
||||
}
|
||||
}
|
||||
|
||||
private setTimer() {
|
||||
this.clearTimer();
|
||||
this.activedTimer = setTimeout(() => this.end(), this.timeGap);
|
||||
}
|
||||
|
||||
private clearTimer() {
|
||||
if (this.activedTimer) {
|
||||
clearTimeout(this.activedTimer);
|
||||
}
|
||||
this.activedTimer = null;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { Component } from 'react';
|
||||
import { observer } from '@recore/obx-react';
|
||||
import Designer from './designer';
|
||||
import DocumentView from './document/document-view';
|
||||
import { observer } from '../../../globals';
|
||||
|
||||
@observer
|
||||
export default class ProjectView extends Component<{ designer: Designer }> {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { ProjectSchema, RootSchema } from './schema';
|
||||
import { EventEmitter } from 'events';
|
||||
import Designer from './designer';
|
||||
import DocumentModel, { isDocumentModel } from './document/document-model';
|
||||
import { ProjectSchema, RootSchema, obx, computed } from '../../../globals';
|
||||
|
||||
export default class Project {
|
||||
private emitter = new EventEmitter();
|
||||
@ -21,10 +20,12 @@ export default class Project {
|
||||
componentsTree: [],
|
||||
...schema,
|
||||
};
|
||||
this.open(this.data.componentsTree[0] || {
|
||||
componentName: 'Page',
|
||||
fileName: '',
|
||||
});
|
||||
this.open(
|
||||
this.data.componentsTree[0] || {
|
||||
componentName: 'Page',
|
||||
fileName: '',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@computed get currentDocument() {
|
||||
@ -106,7 +107,7 @@ export default class Project {
|
||||
}
|
||||
|
||||
checkExclusive(actived: DocumentModel) {
|
||||
this.documents.forEach((doc) => {
|
||||
this.documents.forEach(doc => {
|
||||
if (doc !== actived) {
|
||||
doc.suspense();
|
||||
}
|
||||
@ -115,7 +116,7 @@ export default class Project {
|
||||
}
|
||||
|
||||
closeOthers(opened: DocumentModel) {
|
||||
this.documents.forEach((doc) => {
|
||||
this.documents.forEach(doc => {
|
||||
if (doc !== opened) {
|
||||
doc.close();
|
||||
}
|
||||
@ -131,5 +132,4 @@ export default class Project {
|
||||
// 通知标记删除,需要告知服务端
|
||||
// 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁,
|
||||
// 哪个删除就
|
||||
|
||||
}
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
// 表达式
|
||||
export interface JSExpression {
|
||||
type: 'JSExpression';
|
||||
/**
|
||||
* 表达式字符串
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* 模拟值
|
||||
*/
|
||||
mock?: any;
|
||||
}
|
||||
|
||||
export interface JSSlot {
|
||||
type: 'JSSlot';
|
||||
value: NodeSchema;
|
||||
}
|
||||
|
||||
// JSON 基本类型
|
||||
export type JSONValue = boolean | string | number | null | JSONArray | JSONObject;
|
||||
export type JSONArray = JSONValue[];
|
||||
export interface JSONObject {
|
||||
[key: string]: JSONValue;
|
||||
}
|
||||
|
||||
// 复合类型
|
||||
export type CompositeValue = JSONValue | JSExpression | JSSlot | CompositeArray | CompositeObject;
|
||||
export type CompositeArray = CompositeValue[];
|
||||
export interface CompositeObject {
|
||||
[key: string]: CompositeValue;
|
||||
}
|
||||
|
||||
export interface NpmInfo {
|
||||
componentName?: string;
|
||||
package: string;
|
||||
version: string;
|
||||
destructuring?: boolean;
|
||||
exportName?: string;
|
||||
subName?: string;
|
||||
main?: string;
|
||||
}
|
||||
|
||||
export type ComponentsMap = NpmInfo[];
|
||||
|
||||
export type UtilsMap = Array<
|
||||
| {
|
||||
name: string;
|
||||
type: 'npm';
|
||||
content: NpmInfo;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: '';
|
||||
}
|
||||
>;
|
||||
|
||||
// lang "en-US" | "zh-CN" | "zh-TW" | ...
|
||||
export interface I18nMap {
|
||||
[lang: string]: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface DataSourceConfig {
|
||||
id: string;
|
||||
isInit: boolean;
|
||||
type: string;
|
||||
options: {
|
||||
uri: string;
|
||||
[option: string]: CompositeValue;
|
||||
};
|
||||
[otherKey: string]: CompositeValue;
|
||||
}
|
||||
|
||||
export interface NodeSchema {
|
||||
id?: string;
|
||||
componentName: string;
|
||||
props?: PropsMap | PropsList;
|
||||
leadingComponents?: string;
|
||||
condition?: CompositeValue;
|
||||
loop?: CompositeValue;
|
||||
loopArgs?: [string, string];
|
||||
children?: NodeData | NodeData[];
|
||||
}
|
||||
|
||||
export type PropsMap = CompositeObject;
|
||||
export type PropsList = Array<{
|
||||
spread?: boolean;
|
||||
name?: string;
|
||||
value: CompositeValue;
|
||||
}>;
|
||||
|
||||
export type NodeData = NodeSchema | JSExpression | DOMText;
|
||||
|
||||
export function isJSExpression(data: any): data is JSExpression {
|
||||
return data && data.type === 'JSExpression';
|
||||
}
|
||||
|
||||
export function isJSSlot(data: any): data is JSSlot {
|
||||
return data && data.type === 'JSSlot';
|
||||
}
|
||||
|
||||
export function isDOMText(data: any): data is DOMText {
|
||||
return typeof data === 'string';
|
||||
}
|
||||
|
||||
export type DOMText = string;
|
||||
|
||||
export interface RootSchema extends NodeSchema {
|
||||
componentName: string; // 'Block' | 'Page' | 'Component';
|
||||
fileName: string;
|
||||
meta?: object;
|
||||
state?: {
|
||||
[key: string]: CompositeValue;
|
||||
};
|
||||
methods?: {
|
||||
[key: string]: JSExpression;
|
||||
};
|
||||
lifeCycles?: {
|
||||
[key: string]: JSExpression;
|
||||
};
|
||||
css?: string;
|
||||
dataSource?: {
|
||||
items: DataSourceConfig[];
|
||||
} | any;
|
||||
defaultProps?: CompositeObject;
|
||||
}
|
||||
|
||||
export interface BlockSchema extends RootSchema {
|
||||
componentName: 'Block';
|
||||
}
|
||||
|
||||
export interface PageSchema extends RootSchema {
|
||||
componentName: 'Page';
|
||||
}
|
||||
|
||||
export interface ComponentSchema extends RootSchema {
|
||||
componentName: 'Component';
|
||||
}
|
||||
|
||||
export interface ProjectSchema {
|
||||
version: string;
|
||||
componentsMap: ComponentsMap;
|
||||
componentsTree: RootSchema[];
|
||||
i18n?: I18nMap;
|
||||
utils?: UtilsMap;
|
||||
constants?: JSONObject;
|
||||
css?: string;
|
||||
dataSource?: {
|
||||
items: DataSourceConfig[];
|
||||
};
|
||||
}
|
||||
|
||||
export function isNodeSchema(data: any): data is NodeSchema {
|
||||
return data && data.componentName;
|
||||
}
|
||||
|
||||
export function isProjectSchema(data: any): data is ProjectSchema {
|
||||
return data && data.componentsTree;
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { Component as ReactComponent, ComponentType } from 'react';
|
||||
import { LocateEvent, ISensor } from './helper/dragon';
|
||||
import { ISensor } from './helper/dragon';
|
||||
import { Point } from './helper/location';
|
||||
import Node from './document/node/node';
|
||||
import { ScrollTarget, IScrollable } from './helper/scroller';
|
||||
import { ComponentMetadata } from './component-meta';
|
||||
import { ComponentMetadata } from '../../../globals';
|
||||
|
||||
export type AutoFit = '100%';
|
||||
export const AutoFit = '100%';
|
||||
|
||||
@ -8,7 +8,7 @@ import components from './config/components';
|
||||
import utils from './config/utils';
|
||||
import constants from './config/constants';
|
||||
import './config/locale';
|
||||
import './config/setters';
|
||||
import '../../plugin-setters';
|
||||
|
||||
import './global.scss';
|
||||
import './config/theme.scss';
|
||||
|
||||
@ -1 +1,3 @@
|
||||
shared globals
|
||||
|
||||
发 CDN
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@alifd/next": "^1.19.16",
|
||||
"@recore/obx": "^1.0.8",
|
||||
"@recore/obx-react": "^1.0.7",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16",
|
||||
"react-dom": "^16.7.0"
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import { uniqueId } from '../../../../utils/unique-id';
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { saveTips } from './tip-handler';
|
||||
|
||||
export interface TipConfig {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
theme?: string;
|
||||
direction?: string; // 'n|s|w|e|top|bottom|left|right';
|
||||
}
|
||||
import { TipConfig } from '../../types';
|
||||
|
||||
export default class EmbedTip extends Component<TipConfig> {
|
||||
private id = uniqueId('tips$');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { TipConfig } from './embed-tip';
|
||||
import { EventEmitter } from 'events';
|
||||
import { TipConfig } from '../../types';
|
||||
|
||||
export interface TipOptions extends TipConfig {
|
||||
target: HTMLElement;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { resolvePosition } from './utils';
|
||||
import tipHandler from './tip-handler';
|
||||
import tipHandler, { TipOptions } from './tip-handler';
|
||||
import { intl } from '../../intl';
|
||||
|
||||
export default class Tip extends Component {
|
||||
private dispose?: () => void;
|
||||
@ -100,7 +101,7 @@ export default class Tip extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const tip: any = tipHandler.tip || {};
|
||||
const tip: TipOptions = tipHandler.tip || ({} as any);
|
||||
const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null);
|
||||
|
||||
this.originClassName = className;
|
||||
@ -113,7 +114,7 @@ export default class Tip extends Component {
|
||||
}}
|
||||
>
|
||||
<i className="lc-arrow" />
|
||||
<div className="lc-tip-content">{tip.children}</div>
|
||||
<div className="lc-tip-content">{intl(tip.children)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,27 +1,19 @@
|
||||
import { Component, isValidElement, ReactElement, ReactNode } from 'react';
|
||||
import { Icon } from '@alifd/next';
|
||||
import { Component, isValidElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import EmbedTip, { TipConfig } from '../tip/embed-tip';
|
||||
import EmbedTip from '../tip/embed-tip';
|
||||
import './title.less';
|
||||
import { IconConfig, createIcon } from '../../utils';
|
||||
import { createIcon } from '../../utils';
|
||||
import { TitleContent, isI18nData } from '../../types';
|
||||
import { intl } from '../../intl';
|
||||
|
||||
export interface TitleConfig {
|
||||
label?: ReactNode;
|
||||
tip?: string | ReactElement | TipConfig;
|
||||
icon?: string | ReactElement | IconConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type TitleContent = string | ReactElement | TitleConfig;
|
||||
|
||||
export class Title extends Component<{ title: TitleContent; onClick?: () => void }> {
|
||||
export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> {
|
||||
render() {
|
||||
let { title } = this.props;
|
||||
let { title, className, onClick } = this.props;
|
||||
if (isValidElement(title)) {
|
||||
return title;
|
||||
}
|
||||
if (typeof title === 'string') {
|
||||
title = { label: title }; // tslint:disable-line
|
||||
if (typeof title === 'string' || isI18nData(title)) {
|
||||
title = { label: title };
|
||||
}
|
||||
|
||||
const icon = title.icon ? createIcon(title.icon) : null;
|
||||
@ -32,22 +24,24 @@ export class Title extends Component<{ title: TitleContent; onClick?: () => void
|
||||
tip = title.tip;
|
||||
} else {
|
||||
const tipProps =
|
||||
typeof title.tip === 'object' && !isValidElement(title.tip) ? title.tip : { children: title.tip };
|
||||
typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip))
|
||||
? title.tip
|
||||
: { children: title.tip };
|
||||
tip = <EmbedTip direction="top" theme="black" {...tipProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('lc-title', title.className, {
|
||||
<span
|
||||
className={classNames('lc-title', className, title.className, {
|
||||
'has-tip': !!tip,
|
||||
})}
|
||||
onClick={this.props.onClick}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon ? <div className="lc-title-icon">{icon}</div> : null}
|
||||
{title.label ? <span className="lc-title-label">{title.label}</span> : null}
|
||||
{icon ? <b className="lc-title-icon">{icon}</b> : null}
|
||||
{title.label ? intl(title.label) : null}
|
||||
{tip}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
text-decoration-style: dashed;
|
||||
text-decoration-color: rgba(31, 56, 88, .3);
|
||||
}
|
||||
padding: 2px 0;
|
||||
line-height: initial !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.actived .lc-title {
|
||||
|
||||
2
packages/globals/src/di/index.ts
Normal file
2
packages/globals/src/di/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './setter';
|
||||
export * from './transducer';
|
||||
43
packages/globals/src/di/setter.ts
Normal file
43
packages/globals/src/di/setter.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { CustomView, isCustomView } from '../types/setter-config';
|
||||
import { createContent } from '../../../utils/create-content';
|
||||
import { TitleContent } from '../types';
|
||||
|
||||
export type RegisteredSetter = {
|
||||
component: CustomView;
|
||||
defaultProps?: object;
|
||||
title?: TitleContent;
|
||||
};
|
||||
|
||||
const settersMap = new Map<string, RegisteredSetter>();
|
||||
export function registerSetter(type: string, setter: CustomView | RegisteredSetter) {
|
||||
if (isCustomView(setter)) {
|
||||
setter = {
|
||||
component: setter,
|
||||
title: (setter as any).displayName || (setter as any).name || 'CustomSetter'
|
||||
};
|
||||
}
|
||||
settersMap.set(type, setter);
|
||||
}
|
||||
|
||||
export function getSetter(type: string): RegisteredSetter | null {
|
||||
return settersMap.get(type) || null;
|
||||
}
|
||||
|
||||
export function createSetterContent(setter: any, props: object): ReactNode {
|
||||
if (typeof setter === 'string') {
|
||||
setter = getSetter(setter);
|
||||
if (!setter) {
|
||||
return null;
|
||||
}
|
||||
if (setter.defaultProps) {
|
||||
props = {
|
||||
...setter.defaultProps,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
setter = setter.component;
|
||||
}
|
||||
|
||||
return createContent(setter, props);
|
||||
}
|
||||
12
packages/globals/src/di/transducer.ts
Normal file
12
packages/globals/src/di/transducer.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TransformedComponentMetadata } from '../types';
|
||||
|
||||
export type MetadataTransducer = (prev: TransformedComponentMetadata) => TransformedComponentMetadata;
|
||||
const metadataTransducers: MetadataTransducer[] = [];
|
||||
|
||||
export function registerMetadataTransducer(transducer: MetadataTransducer) {
|
||||
metadataTransducers.push(transducer);
|
||||
}
|
||||
|
||||
export function getRegisteredMetadataTransducers(): MetadataTransducer[] {
|
||||
return metadataTransducers;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { IconBase, IconBaseProps } from "./icon-base";
|
||||
|
||||
export function Clone(props: IconBaseProps) {
|
||||
export function IconClone(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M192 256.16C192 220.736 220.704 192 256.16 192h639.68C931.264 192 960 220.704 960 256.16v639.68A64.16 64.16 0 0 1 895.84 960H256.16A64.16 64.16 0 0 1 192 895.84V256.16z m64 31.584v576.512a32 32 0 0 0 31.744 31.744h576.512a32 32 0 0 0 31.744-31.744V287.744A32 32 0 0 0 864.256 256H287.744A32 32 0 0 0 256 287.744zM288 192v64h64V192H288z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m128 0v64h64V192h-64z m96 96v64h64V288h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m0 128v64h64v-64h-64z m-96 96v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64h-64z m-128 0v64h64v-64H288z m-96-96v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64v-64H192z m0-128v64h64V288H192z m160 416c0-17.664 14.592-32 32.064-32h319.872a31.968 31.968 0 1 1 0 64h-319.872A31.968 31.968 0 0 1 352 704z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 576z m0-128c0-17.664 14.4-32 32.224-32h383.552c17.792 0 32.224 14.208 32.224 32 0 17.664-14.4 32-32.224 32H384.224A32.032 32.032 0 0 1 352 448z m512 47.936V192h-64V159.968A31.776 31.776 0 0 0 768.032 128H160A31.776 31.776 0 0 0 128 159.968V768c0 17.92 14.304 31.968 31.968 31.968H192v64h303.936H128.128A63.968 63.968 0 0 1 64 799.872V128.128C64 92.704 92.48 64 128.128 64h671.744C835.296 64 864 92.48 864 128.128v367.808z"/>
|
||||
@ -8,4 +8,4 @@ export function Clone(props: IconBaseProps) {
|
||||
);
|
||||
}
|
||||
|
||||
Clone.displayName = 'Clone';
|
||||
IconClone.displayName = 'Clone';
|
||||
|
||||
10
packages/globals/src/icons/component.tsx
Normal file
10
packages/globals/src/icons/component.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { IconBase, IconBaseProps } from "./icon-base";
|
||||
|
||||
export function IconComponent(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M783.5648 437.4528h-18.0224V336.6912c0-43.8272-35.6352-79.4624-79.4624-79.4624h-110.592V241.664c0-90.9312-73.728-164.6592-164.6592-164.6592-90.9312 0-164.6592 73.728-164.6592 164.6592v15.5648H155.2384c-43.8272 0-79.4624 35.6352-79.4624 79.4624v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h56.1152c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192H106.496c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 43.8272 35.6352 79.4624 79.4624 79.4624h531.2512c43.8272 0 79.4624-35.6352 79.4624-79.4624v-100.7616h18.0224c90.9312 0 164.6592-73.728 164.6592-164.6592-0.4096-90.9312-74.1376-164.6592-165.0688-164.6592z m0 267.8784h-48.7424c-16.7936 0-30.72 13.9264-30.72 30.72v131.4816c0 9.8304-8.192 18.0224-18.0224 18.0224H155.2384c-9.8304 0-18.0224-8.192-18.0224-18.0224v-100.7616h25.3952c90.9312 0 164.6592-73.728 164.6592-164.6592 0-90.9312-73.728-164.6592-164.6592-164.6592h-25.3952V336.6912c0-9.8304 8.192-18.0224 18.0224-18.0224h121.6512c16.7936 0 30.72-13.9264 30.72-30.72V241.664c0-56.9344 46.2848-103.2192 103.2192-103.2192s103.2192 46.2848 103.2192 103.2192v46.2848c0 16.7936 13.9264 30.72 30.72 30.72h141.312c9.8304 0 18.0224 8.192 18.0224 18.0224v131.4816c0 16.7936 13.9264 30.72 30.72 30.72h48.7424c56.9344 0 103.2192 46.2848 103.2192 103.2192s-46.2848 103.2192-103.2192 103.2192z" />
|
||||
</IconBase>
|
||||
);
|
||||
}
|
||||
IconComponent.displayName = 'Component';
|
||||
11
packages/globals/src/icons/container.tsx
Normal file
11
packages/globals/src/icons/container.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { IconBase, IconBaseProps } from "./icon-base";
|
||||
|
||||
export function IconContainer(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M800 800h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-256 0h64v64h-64v-64z m0-640h64v64h-64v-64z m128 640h64v64h-64v-64zM160 672h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m640 384h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m0-128h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z m-128 0h64v64h-64v-64z" />
|
||||
<path d="M896 64H128c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V128c0-35.2-28.8-64-64-64z m0 800c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V160c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v704z" />
|
||||
</IconBase>
|
||||
);
|
||||
}
|
||||
IconContainer.displayName = 'Container';
|
||||
@ -1,10 +1,10 @@
|
||||
import { IconBase, IconBaseProps } from "./icon-base";
|
||||
|
||||
export function Hidden(props: IconBaseProps) {
|
||||
export function IconHidden(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M183.423543 657.078213l163.499771-98.484012c-4.233418-14.908548-6.646374-30.585599-6.646374-46.852074 0-94.665033 76.739778-171.404812 171.404812-171.404812 45.983287 0 87.641059 18.20871 118.42518 47.679929l129.791042-78.17957c-73.254398-41.73145-157.866471-65.812915-248.216221-65.812915-192.742792 0-360.068705 108.505249-444.453604 267.715321C96.636944 567.228859 136.301316 616.355743 183.423543 657.078213zM841.253886 367.552144l-164.382884 99.015108c3.934612 14.415314 6.215562 29.513174 6.215562 45.174875 0 94.665033-76.739778 171.404812-171.404812 171.404812-45.361117 0-86.484723-17.747199-117.142977-46.515407l-129.419582 77.955466c72.874751 41.149189 156.893306 64.871473 246.563582 64.871473 192.742792 0 360.068705-108.505249 444.453604-267.717368C927.000805 456.773188 887.794875 408.054603 841.253886 367.552144zM420.280042 511.741104c0 0.550539 0.152473 1.060145 0.161682 1.608637l135.080511-81.366146c-13.065574-7.198959-27.854395-11.658528-43.826158-11.658528C461.20922 420.325068 420.280042 461.254246 420.280042 511.741104zM447.739441 576.947198l69.02098-41.574884L948.364369 275.395234c10.812253-6.512321 14.297634-20.558222 7.785314-31.369452-6.512321-10.812253-20.556175-14.296611-31.368428-7.785314L575.654762 446.537056l0 0-151.20577 91.078345 0 0L75.027787 748.090043c-10.812253 6.512321-14.297634 20.556175-7.785314 31.368428 6.512321 10.812253 20.556175 14.297634 31.369452 7.785314L447.739441 576.947198 447.739441 576.947198zM511.696078 603.157139c50.487881 0 91.416036-40.928155 91.416036-91.416036 0-0.549515-0.152473-1.057075-0.161682-1.605567l-135.079488 81.364099C480.935494 598.699618 495.724315 603.157139 511.696078 603.157139z" />
|
||||
</IconBase>
|
||||
);
|
||||
}
|
||||
Hidden.displayName = 'Hidden';
|
||||
IconHidden.displayName = 'Hidden';
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
export * from './clone';
|
||||
export * from './hidden';
|
||||
export * from './remove';
|
||||
export * from './settings';
|
||||
export * from './setting';
|
||||
export * from './icon-base';
|
||||
export * from './component';
|
||||
export * from './container';
|
||||
export * from './page';
|
||||
|
||||
12
packages/globals/src/icons/page.tsx
Normal file
12
packages/globals/src/icons/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { IconBase, IconBaseProps } from "./icon-base";
|
||||
|
||||
export function IconPage(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M381.6 864H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0.1-5.7-5.5-10.3-12.3-10.3zM382 780.6H162c-6.9 0-12.5 4.6-12.5 10.3v19.3c0 5.7 5.6 10.3 12.5 10.3h220c6.9 0 12.5-4.6 12.5-10.3v-19.3c0-5.7-5.6-10.3-12.5-10.3zM162.4 737.2h219.1c6.8 0 12.4-4.6 12.4-10.3v-19.3c0-5.7-5.6-10.3-12.4-10.3H162.4c-6.9 0-12.4 4.6-12.4 10.3v19.3c0 5.7 5.6 10.3 12.4 10.3z" />
|
||||
<path d="M977.1 0H46.9C21 0 0 21 0 46.9v930.2c0 25.9 21 46.9 46.9 46.9h930.2c25.9 0 46.9-21 46.9-46.9V46.9C1024 21 1003 0 977.1 0z m-18.7 911.6c0 25.9-21 46.9-46.9 46.9H112.4c-25.9 0-46.9-21-46.9-47V112.4c0-25.9 21-46.9 46.9-46.9h799.1c25.9 0 46.9 21 46.9 46.9v799.2z" />
|
||||
<path d="M207.9 342.7h608.2c32 0 57.9-25.9 57.9-57.9v-83c0-32-25.9-57.9-57.9-57.9H207.9c-32 0-57.9 25.9-57.9 57.9v83c0 32 25.9 57.9 57.9 57.9zM200 201.8c0-4.4 3.5-7.9 7.9-7.9h608.2c4.4 0 7.9 3.5 7.9 7.9v83c0 4.4-3.5 7.9-7.9 7.9H207.9c-4.4 0-7.9-3.5-7.9-7.9v-83zM806.4 405.7h-277c-37.3 0-67.6 30.2-67.6 67.6v363.2c0 37.3 30.2 67.6 67.6 67.6h277c37.3 0 67.6-30.2 67.6-67.6V473.3c0-37.4-30.2-67.6-67.6-67.6zM824 836.4c0 9.7-7.9 17.6-17.6 17.6h-277c-9.7 0-17.6-7.9-17.6-17.6V473.3c0-9.7 7.9-17.6 17.6-17.6h277c9.7 0 17.6 7.9 17.6 17.6v363.1zM272 649.7c67.4 0 122-54.6 122-122s-54.6-122-122-122-122 54.6-122 122 54.6 122 122 122z m0-204c45.2 0 82 36.8 82 82s-36.8 82-82 82-82-36.8-82-82 36.8-82 82-82z" />
|
||||
</IconBase>
|
||||
);
|
||||
}
|
||||
IconPage.displayName = 'Page';
|
||||
@ -1,10 +1,10 @@
|
||||
import { IconBase, IconBaseProps } from './icon-base';
|
||||
|
||||
export function Remove(props: IconBaseProps) {
|
||||
export function IconRemove(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M224 256v639.84A64 64 0 0 0 287.84 960h448.32A64 64 0 0 0 800 895.84V256h64a32 32 0 1 0 0-64H160a32 32 0 1 0 0 64h64zM384 96c0-17.664 14.496-32 31.904-32h192.192C625.696 64 640 78.208 640 96c0 17.664-14.496 32-31.904 32H415.904A31.872 31.872 0 0 1 384 96z m-96 191.744C288 270.208 302.4 256 320.224 256h383.552C721.6 256 736 270.56 736 287.744v576.512C736 881.792 721.6 896 703.776 896H320.224A32.224 32.224 0 0 1 288 864.256V287.744zM352 352c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z" />
|
||||
</IconBase>
|
||||
);
|
||||
}
|
||||
Remove.displayName = 'Remove';
|
||||
IconRemove.displayName = 'Remove';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { IconBase, IconBaseProps } from './icon-base';
|
||||
|
||||
export function Setting(props: IconBaseProps) {
|
||||
export function IconSetting(props: IconBaseProps) {
|
||||
return (
|
||||
<IconBase viewBox="0 0 1024 1024" {...props}>
|
||||
<path d="M965.824 405.952a180.48 180.48 0 0 1-117.12-85.376 174.464 174.464 0 0 1-16-142.08 22.208 22.208 0 0 0-7.04-23.552 480.576 480.576 0 0 0-153.6-89.216 23.104 23.104 0 0 0-24.32 5.76 182.208 182.208 0 0 1-135.68 57.92 182.208 182.208 0 0 1-133.12-56.64 23.104 23.104 0 0 0-26.88-7.04 478.656 478.656 0 0 0-153.6 89.856 22.208 22.208 0 0 0-7.04 23.552 174.464 174.464 0 0 1-16 141.44A180.48 180.48 0 0 1 58.24 405.952a22.4 22.4 0 0 0-17.28 17.792 455.08 455.08 0 0 0 0 176.512 22.4 22.4 0 0 0 17.28 17.792 180.48 180.48 0 0 1 117.12 84.736c25.408 42.944 31.232 94.592 16 142.08a22.208 22.208 0 0 0 7.04 23.552A480.576 480.576 0 0 0 352 957.632h7.68a23.04 23.04 0 0 0 16.64-7.04 184.128 184.128 0 0 1 266.944 0c6.592 8.96 18.752 11.968 28.8 7.04a479.36 479.36 0 0 0 156.16-88.576 22.208 22.208 0 0 0 7.04-23.552 174.464 174.464 0 0 1 13.44-142.72 180.48 180.48 0 0 1 117.12-84.736 22.4 22.4 0 0 0 17.28-17.792 452.613 452.613 0 0 0 0-176.512 23.04 23.04 0 0 0-17.28-17.792z m-42.88 169.408a218.752 218.752 0 0 0-128 98.112 211.904 211.904 0 0 0-21.76 156.736 415.936 415.936 0 0 1-112 63.68 217.472 217.472 0 0 0-149.12-63.68 221.312 221.312 0 0 0-149.12 63.68 414.592 414.592 0 0 1-112-63.68c12.8-53.12 4.288-109.12-23.68-156.096A218.752 218.752 0 0 0 101.12 575.36a386.176 386.176 0 0 1 0-127.36 218.752 218.752 0 0 0 128-98.112c27.2-47.552 34.944-103.68 21.76-156.8a415.296 415.296 0 0 1 112-63.68A221.44 221.44 0 0 0 512 187.392a218.24 218.24 0 0 0 149.12-57.984 413.952 413.952 0 0 1 112 63.744 211.904 211.904 0 0 0 23.04 156.096 218.752 218.752 0 0 0 128 98.112 386.65 386.65 0 0 1 0 127.36l-1.28 0.64z" />
|
||||
@ -9,4 +9,4 @@ export function Setting(props: IconBaseProps) {
|
||||
);
|
||||
}
|
||||
|
||||
Setting.displayName = 'Setting';
|
||||
IconSetting.displayName = 'Setting';
|
||||
@ -2,3 +2,6 @@ export * from './intl';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './icons';
|
||||
export * from './types';
|
||||
export * from './di';
|
||||
export * from './obx';
|
||||
|
||||
@ -36,6 +36,9 @@ class AliGlobalLocale {
|
||||
}
|
||||
|
||||
setLocale(locale: string) {
|
||||
if (locale === this.locale) {
|
||||
return;
|
||||
}
|
||||
this.locale = locale;
|
||||
if (hasLocalStorage(window)) {
|
||||
const store = window.localStorage;
|
||||
@ -54,6 +57,7 @@ class AliGlobalLocale {
|
||||
|
||||
store.setItem(LowcodeConfigKey, JSON.stringify(config));
|
||||
}
|
||||
this.emitter.emit('localechange', locale);
|
||||
}
|
||||
|
||||
getLocale() {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { globalLocale } from './ali-global-locale';
|
||||
import { PureComponent, ReactNode } from 'react';
|
||||
import { isI18nData } from '../types';
|
||||
|
||||
function injectVars(template: string, params: any): string {
|
||||
if (!template || !params) {
|
||||
@ -13,14 +14,20 @@ function injectVars(template: string, params: any): string {
|
||||
return $1;
|
||||
});
|
||||
}
|
||||
|
||||
export interface I18nData {
|
||||
type: 'i18n';
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export function isI18nData(obj: any): obj is I18nData {
|
||||
return obj && obj.type === 'i18n';
|
||||
function generateTryLocales(locale: string) {
|
||||
const tries = [locale, locale.replace('-', '_')];
|
||||
if (locale === 'zh-TW' || locale === 'en-US') {
|
||||
tries.push('zh-CN');
|
||||
tries.push('zh_CN');
|
||||
} else {
|
||||
tries.push('en-US');
|
||||
tries.push('en_US');
|
||||
if (locale !== 'zh-CN') {
|
||||
tries.push('zh-CN');
|
||||
tries.push('zh_CN');
|
||||
}
|
||||
}
|
||||
return tries;
|
||||
}
|
||||
|
||||
export function localeFormat(data: any, params?: object): string {
|
||||
@ -28,9 +35,16 @@ export function localeFormat(data: any, params?: object): string {
|
||||
return data;
|
||||
}
|
||||
const locale = globalLocale.getLocale();
|
||||
const tpl = data[locale];
|
||||
const tries = generateTryLocales(locale);
|
||||
let tpl: string | undefined;
|
||||
for (const lan of tries) {
|
||||
tpl = data[lan];
|
||||
if (tpl != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tpl == null) {
|
||||
return `##intl null@${locale}##`;
|
||||
return `##intl@${locale}##`;
|
||||
}
|
||||
return injectVars(tpl, params);
|
||||
}
|
||||
@ -53,11 +67,22 @@ export function intl(data: any, params?: object): ReactNode {
|
||||
return data;
|
||||
}
|
||||
|
||||
export function shallowIntl(data: any): any {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data;
|
||||
}
|
||||
const maps: any = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
maps[key] = localeFormat(data[key]);
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
export function createIntl(
|
||||
instance: string | object,
|
||||
): {
|
||||
intl(id: string, params?: object): ReactNode;
|
||||
getIntlString(id: string, params?: object): string;
|
||||
intlString(id: string, params?: object): string;
|
||||
getLocale(): string;
|
||||
setLocale(locale: string): void;
|
||||
} {
|
||||
@ -79,11 +104,12 @@ export function createIntl(
|
||||
|
||||
useLocale(globalLocale.getLocale());
|
||||
|
||||
function getIntlString(key: string, params?: object): string {
|
||||
function intlString(key: string, params?: object): string {
|
||||
// TODO: tries lost language
|
||||
const str = data[key];
|
||||
|
||||
if (str == null) {
|
||||
return `##intl null@${key}##`;
|
||||
return `##intl@${key}##`;
|
||||
}
|
||||
|
||||
return injectVars(str, params);
|
||||
@ -101,7 +127,7 @@ export function createIntl(
|
||||
}
|
||||
render() {
|
||||
const { id, params } = this.props;
|
||||
return getIntlString(id, params);
|
||||
return intlString(id, params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +135,7 @@ export function createIntl(
|
||||
intl(id: string, params?: object) {
|
||||
return <Intl id={id} params={params} />;
|
||||
},
|
||||
getIntlString,
|
||||
intlString,
|
||||
getLocale() {
|
||||
return globalLocale.getLocale();
|
||||
},
|
||||
|
||||
4
packages/globals/src/obx/index.ts
Normal file
4
packages/globals/src/obx/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from '@recore/obx';
|
||||
import { observer } from '@recore/obx-react';
|
||||
|
||||
export { observer };
|
||||
16
packages/globals/src/types/data-source.ts
Normal file
16
packages/globals/src/types/data-source.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { CompositeValue } from './value-type';
|
||||
|
||||
export interface DataSourceConfig {
|
||||
id: string;
|
||||
isInit: boolean;
|
||||
type: string;
|
||||
options: {
|
||||
uri: string;
|
||||
[option: string]: CompositeValue;
|
||||
};
|
||||
[otherKey: string]: CompositeValue;
|
||||
}
|
||||
|
||||
export interface DataSource {
|
||||
items: DataSourceConfig[];
|
||||
}
|
||||
58
packages/globals/src/types/field-config.ts
Normal file
58
packages/globals/src/types/field-config.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { TitleContent } from './title';
|
||||
import { SetterType } from './setter-config';
|
||||
|
||||
|
||||
export interface FieldExtraProps {
|
||||
/**
|
||||
* 是否必填参数
|
||||
*/
|
||||
isRequired?: boolean;
|
||||
/**
|
||||
* default value of target prop for setter use
|
||||
*/
|
||||
defaultValue?: any;
|
||||
getValue?: (field: any, fieldValue: any) => any;
|
||||
setValue?: (field: any, value: any) => void;
|
||||
/**
|
||||
* the field conditional show, is not set always true
|
||||
* @default undefined
|
||||
*/
|
||||
condition?: (field: any) => boolean;
|
||||
/**
|
||||
* default collapsed when display accordion
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* important field
|
||||
*/
|
||||
important?: boolean;
|
||||
/**
|
||||
* internal use
|
||||
*/
|
||||
forceInline?: number;
|
||||
}
|
||||
|
||||
export interface FieldConfig extends FieldExtraProps {
|
||||
type?: 'field' | 'group';
|
||||
/**
|
||||
* the name of this setting field, which used in quickEditor
|
||||
*/
|
||||
name: string | number;
|
||||
/**
|
||||
* the field title
|
||||
* @default sameas .name
|
||||
*/
|
||||
title?: TitleContent;
|
||||
/**
|
||||
* the field body contains when .type = 'field'
|
||||
*/
|
||||
setter?: SetterType;
|
||||
/**
|
||||
* the setting items which group body contains when .type = 'group'
|
||||
*/
|
||||
items?: FieldConfig[];
|
||||
/**
|
||||
* extra props for field
|
||||
*/
|
||||
extraProps?: FieldExtraProps;
|
||||
}
|
||||
13
packages/globals/src/types/i18n.ts
Normal file
13
packages/globals/src/types/i18n.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface I18nData {
|
||||
type: 'i18n';
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// type checks
|
||||
export function isI18nData(obj: any): obj is I18nData {
|
||||
return obj && obj.type === 'i18n';
|
||||
}
|
||||
|
||||
export interface I18nMap {
|
||||
[lang: string]: { [key: string]: string };
|
||||
}
|
||||
9
packages/globals/src/types/icon.ts
Normal file
9
packages/globals/src/types/icon.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ReactElement, ComponentType } from 'react';
|
||||
|
||||
export interface IconConfig {
|
||||
type: string;
|
||||
size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type IconType = string | ReactElement | ComponentType<any> | IconConfig;
|
||||
13
packages/globals/src/types/index.ts
Normal file
13
packages/globals/src/types/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export * from './data-source';
|
||||
export * from './field-config';
|
||||
export * from './i18n';
|
||||
export * from './icon';
|
||||
export * from './metadata';
|
||||
export * from './npm';
|
||||
export * from './prop-config';
|
||||
export * from './schema';
|
||||
export * from './tip';
|
||||
export * from './title';
|
||||
export * from './utils';
|
||||
export * from './value-type';
|
||||
export * from './setter-config';
|
||||
87
packages/globals/src/types/metadata.ts
Normal file
87
packages/globals/src/types/metadata.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { IconType } from './icon';
|
||||
import { TipContent } from './tip';
|
||||
import { TitleContent } from './title';
|
||||
import { PropConfig } from './prop-config';
|
||||
import { NpmInfo } from './npm';
|
||||
import { FieldConfig } from './field-config';
|
||||
|
||||
export interface NestingRule {
|
||||
childWhitelist?: string[];
|
||||
parentWhitelist?: string[];
|
||||
descendantBlacklist?: string[];
|
||||
ancestorWhitelist?: string[];
|
||||
}
|
||||
|
||||
export interface ComponentConfigure {
|
||||
isContainer?: boolean;
|
||||
isModal?: boolean;
|
||||
isNullNode?: boolean;
|
||||
descriptor?: string;
|
||||
nestingRule?: NestingRule;
|
||||
rectSelector?: string;
|
||||
// copy,move,delete
|
||||
disableBehaviors?: string[];
|
||||
actions?: ComponentAction[];
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
props?: FieldConfig[];
|
||||
styles?: object;
|
||||
events?: object;
|
||||
component?: ComponentConfigure;
|
||||
}
|
||||
|
||||
export interface ActionContentObject {
|
||||
// 图标
|
||||
icon?: IconType;
|
||||
// 描述
|
||||
description?: TipContent;
|
||||
// 执行动作
|
||||
action?: (node: any) => void;
|
||||
}
|
||||
|
||||
export interface ComponentAction {
|
||||
// behaviorName
|
||||
name: string;
|
||||
// 菜单名称
|
||||
content: string | ReactNode | ActionContentObject;
|
||||
// 子集
|
||||
items?: ComponentAction[];
|
||||
// 不显示
|
||||
condition?: boolean | ((node: any) => boolean);
|
||||
// 显示在工具条上
|
||||
important?: boolean;
|
||||
}
|
||||
|
||||
export function isActionContentObject(obj: any): obj is ActionContentObject {
|
||||
return obj && typeof obj === 'object';
|
||||
}
|
||||
|
||||
export interface ComponentMetadata {
|
||||
componentName: string;
|
||||
/**
|
||||
* unique id
|
||||
*/
|
||||
uri?: string;
|
||||
/**
|
||||
* title or description
|
||||
*/
|
||||
title?: TitleContent;
|
||||
/**
|
||||
* svg icon for component
|
||||
*/
|
||||
icon?: IconType;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
docUrl?: string;
|
||||
screenshot?: string;
|
||||
devMode?: 'procode' | 'lowcode';
|
||||
npm?: NpmInfo;
|
||||
props?: PropConfig[];
|
||||
configure?: FieldConfig[] | Configure;
|
||||
}
|
||||
|
||||
export interface TransformedComponentMetadata extends ComponentMetadata {
|
||||
configure: Configure & { combined?: FieldConfig[] };
|
||||
}
|
||||
11
packages/globals/src/types/npm.ts
Normal file
11
packages/globals/src/types/npm.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface NpmInfo {
|
||||
componentName?: string;
|
||||
package: string;
|
||||
version: string;
|
||||
destructuring?: boolean;
|
||||
exportName?: string;
|
||||
subName?: string;
|
||||
main?: string;
|
||||
}
|
||||
|
||||
export type ComponentsMap = NpmInfo[];
|
||||
80
packages/globals/src/types/schema.ts
Normal file
80
packages/globals/src/types/schema.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { ComponentsMap } from './npm';
|
||||
import { CompositeValue, JSExpression, CompositeObject, JSONObject } from './value-type';
|
||||
import { DataSource } from './data-source';
|
||||
import { I18nMap } from './i18n';
|
||||
import { UtilsMap } from './utils';
|
||||
|
||||
export interface NodeSchema {
|
||||
id?: string;
|
||||
componentName: string;
|
||||
props?: PropsMap | PropsList;
|
||||
leadingComponents?: string;
|
||||
condition?: CompositeValue;
|
||||
loop?: CompositeValue;
|
||||
loopArgs?: [string, string];
|
||||
children?: NodeData | NodeData[];
|
||||
}
|
||||
|
||||
export type PropsMap = CompositeObject;
|
||||
export type PropsList = Array<{
|
||||
spread?: boolean;
|
||||
name?: string;
|
||||
value: CompositeValue;
|
||||
}>;
|
||||
|
||||
export type NodeData = NodeSchema | JSExpression | DOMText;
|
||||
|
||||
export function isDOMText(data: any): data is DOMText {
|
||||
return typeof data === 'string';
|
||||
}
|
||||
|
||||
export type DOMText = string;
|
||||
|
||||
export interface RootSchema extends NodeSchema {
|
||||
componentName: string; // 'Block' | 'Page' | 'Component';
|
||||
fileName: string;
|
||||
meta?: object;
|
||||
state?: {
|
||||
[key: string]: CompositeValue;
|
||||
};
|
||||
methods?: {
|
||||
[key: string]: JSExpression;
|
||||
};
|
||||
lifeCycles?: {
|
||||
[key: string]: JSExpression;
|
||||
};
|
||||
css?: string;
|
||||
dataSource?: DataSource;
|
||||
defaultProps?: CompositeObject;
|
||||
}
|
||||
|
||||
export interface BlockSchema extends RootSchema {
|
||||
componentName: 'Block';
|
||||
}
|
||||
|
||||
export interface PageSchema extends RootSchema {
|
||||
componentName: 'Page';
|
||||
}
|
||||
|
||||
export interface ComponentSchema extends RootSchema {
|
||||
componentName: 'Component';
|
||||
}
|
||||
|
||||
export interface ProjectSchema {
|
||||
version: string;
|
||||
componentsMap: ComponentsMap;
|
||||
componentsTree: RootSchema[];
|
||||
i18n?: I18nMap;
|
||||
utils?: UtilsMap;
|
||||
constants?: JSONObject;
|
||||
css?: string;
|
||||
dataSource?: DataSource;
|
||||
}
|
||||
|
||||
export function isNodeSchema(data: any): data is NodeSchema {
|
||||
return data && data.componentName;
|
||||
}
|
||||
|
||||
export function isProjectSchema(data: any): data is ProjectSchema {
|
||||
return data && data.componentsTree;
|
||||
}
|
||||
34
packages/globals/src/types/setter-config.ts
Normal file
34
packages/globals/src/types/setter-config.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { isReactComponent } from '../utils';
|
||||
import { ComponentType, ReactElement, isValidElement } from 'react';
|
||||
|
||||
export type CustomView = ReactElement | ComponentType<any>;
|
||||
|
||||
export type DynamicProps = (field: any) => object;
|
||||
|
||||
export interface SetterConfig {
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name
|
||||
*/
|
||||
componentName: string | CustomView;
|
||||
/**
|
||||
* the props pass to Setter Component
|
||||
*/
|
||||
props?: object | DynamicProps;
|
||||
children?: any;
|
||||
isRequired?: boolean;
|
||||
initialValue?: any | ((field: any) => any);
|
||||
}
|
||||
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name, future support blockSchema
|
||||
*/
|
||||
export type SetterType = SetterConfig | string | CustomView;
|
||||
|
||||
|
||||
export function isSetterConfig(obj: any): obj is SetterConfig {
|
||||
return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj);
|
||||
}
|
||||
|
||||
export function isCustomView(obj: any): obj is CustomView {
|
||||
return obj && (isValidElement(obj) || isReactComponent(obj));
|
||||
}
|
||||
11
packages/globals/src/types/tip.ts
Normal file
11
packages/globals/src/types/tip.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { I18nData } from './i18n';
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
|
||||
export interface TipConfig {
|
||||
className?: string;
|
||||
children?: I18nData | ReactNode;
|
||||
theme?: string;
|
||||
direction?: string; // 'n|s|w|e|top|bottom|left|right';
|
||||
}
|
||||
|
||||
export type TipContent = string | I18nData | ReactElement | TipConfig;
|
||||
15
packages/globals/src/types/title.ts
Normal file
15
packages/globals/src/types/title.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import { I18nData } from './i18n';
|
||||
import { TipContent } from './tip';
|
||||
import { IconType } from './icon';
|
||||
|
||||
|
||||
export interface TitleConfig {
|
||||
label?: I18nData | ReactNode;
|
||||
tip?: TipContent;
|
||||
icon?: IconType;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type TitleContent = string | I18nData | ReactElement | TitleConfig;
|
||||
|
||||
13
packages/globals/src/types/utils.ts
Normal file
13
packages/globals/src/types/utils.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NpmInfo } from './npm';
|
||||
|
||||
export type UtilsMap = Array<
|
||||
| {
|
||||
name: string;
|
||||
type: 'npm';
|
||||
content: NpmInfo;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
type: '';
|
||||
}
|
||||
>;
|
||||
42
packages/globals/src/types/value-type.ts
Normal file
42
packages/globals/src/types/value-type.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { NodeSchema } from './schema';
|
||||
|
||||
// 表达式
|
||||
export interface JSExpression {
|
||||
type: 'JSExpression';
|
||||
/**
|
||||
* 表达式字符串
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* 模拟值
|
||||
*/
|
||||
mock?: any;
|
||||
}
|
||||
|
||||
export interface JSSlot {
|
||||
type: 'JSSlot';
|
||||
value: NodeSchema;
|
||||
}
|
||||
|
||||
// JSON 基本类型
|
||||
export type JSONValue = boolean | string | number | null | JSONArray | JSONObject;
|
||||
export type JSONArray = JSONValue[];
|
||||
export interface JSONObject {
|
||||
[key: string]: JSONValue;
|
||||
}
|
||||
|
||||
// 复合类型
|
||||
export type CompositeValue = JSONValue | JSExpression | JSSlot | CompositeArray | CompositeObject;
|
||||
export type CompositeArray = CompositeValue[];
|
||||
export interface CompositeObject {
|
||||
[key: string]: CompositeValue;
|
||||
}
|
||||
|
||||
|
||||
export function isJSExpression(data: any): data is JSExpression {
|
||||
return data && data.type === 'JSExpression';
|
||||
}
|
||||
|
||||
export function isJSSlot(data: any): data is JSSlot {
|
||||
return data && data.type === 'JSSlot';
|
||||
}
|
||||
@ -1,18 +1,14 @@
|
||||
import { Icon } from '@alifd/next';
|
||||
import { isValidElement, ReactNode, ComponentType, createElement, cloneElement, ReactElement } from 'react';
|
||||
import { isValidElement, ReactNode, createElement, cloneElement } from 'react';
|
||||
import { isReactComponent } from './is-react';
|
||||
|
||||
export interface IconConfig {
|
||||
type: string;
|
||||
size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type IconType = string | ReactElement | ComponentType<any> | IconConfig;
|
||||
import { IconType } from '../types';
|
||||
|
||||
const URL_RE = /^(https?:)\/\//i;
|
||||
|
||||
export function createIcon(icon: IconType, props?: object): ReactNode {
|
||||
export function createIcon(icon?: IconType | null, props?: object): ReactNode {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
if (typeof icon === 'string') {
|
||||
if (URL_RE.test(icon)) {
|
||||
return <img src={icon} {...props} />;
|
||||
@ -26,9 +22,5 @@ export function createIcon(icon: IconType, props?: object): ReactNode {
|
||||
return createElement(icon, {...props});
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
return <Icon {...icon} {...props} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return <Icon {...icon} {...props} />;
|
||||
}
|
||||
|
||||
6
packages/plugin-outline-pane/.eslintignore
Normal file
6
packages/plugin-outline-pane/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
build/
|
||||
.*
|
||||
~*
|
||||
node_modules
|
||||
3
packages/plugin-outline-pane/.eslintrc
Normal file
3
packages/plugin-outline-pane/.eslintrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./node_modules/@recore/config/.eslintrc"
|
||||
}
|
||||
6
packages/plugin-outline-pane/.prettierrc
Normal file
6
packages/plugin-outline-pane/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
43
packages/plugin-outline-pane/package.json
Normal file
43
packages/plugin-outline-pane/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@ali/lowcode-plugin-outline-pane",
|
||||
"version": "0.0.0",
|
||||
"description": "xxx for Ali lowCode engine",
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "ava",
|
||||
"test:snapshot": "ava --update-snapshots"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alifd/next": "^1.19.16",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16",
|
||||
"react-dom": "^16.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@recore/config": "^2.0.0",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/node": "^13.7.1",
|
||||
"@types/react": "^16",
|
||||
"@types/react-dom": "^16",
|
||||
"eslint": "^6.5.1",
|
||||
"prettier": "^1.18.2",
|
||||
"tslib": "^1.9.3",
|
||||
"typescript": "^3.1.3",
|
||||
"ts-node": "^8.0.1"
|
||||
},
|
||||
"ava": {
|
||||
"compileEnhancements": false,
|
||||
"snapshotDir": "test/fixtures/__snapshots__",
|
||||
"extensions": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
]
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
1
packages/plugin-outline-pane/src/README.md
Normal file
1
packages/plugin-outline-pane/src/README.md
Normal file
@ -0,0 +1 @@
|
||||
大纲树
|
||||
37
packages/plugin-outline-pane/src/helper/dwell-timer.ts
Normal file
37
packages/plugin-outline-pane/src/helper/dwell-timer.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 停留检查计时器
|
||||
*/
|
||||
export default class DwellTimer {
|
||||
private timer: number | undefined;
|
||||
private previous: any;
|
||||
|
||||
constructor(readonly timeout: number = 400) {}
|
||||
|
||||
/**
|
||||
* 根据传入的 ID 判断,停留事件是否触发
|
||||
* 如果上一次的标示(包括不存在)和这次不相同,则设置停留计时器
|
||||
* 反之什么也不用做
|
||||
*/
|
||||
start(id: any, fn: () => void) {
|
||||
if (this.previous !== id) {
|
||||
this.end();
|
||||
this.previous = id;
|
||||
this.timer = setTimeout(() => {
|
||||
fn();
|
||||
this.end();
|
||||
}, this.timeout) as number;
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
const timer = this.timer;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
|
||||
if (this.previous) {
|
||||
this.previous = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/plugin-outline-pane/src/helper/is-container.ts
Normal file
13
packages/plugin-outline-pane/src/helper/is-container.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { INode, isElementNode, isRootNode } from '../../../../document/node';
|
||||
|
||||
export function isContainer(node: INode): boolean {
|
||||
if (isRootNode(node)) {
|
||||
return true;
|
||||
}
|
||||
if (isElementNode(node)) {
|
||||
// TODO: check from prototype
|
||||
// block Fragment
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
111
packages/plugin-outline-pane/src/helper/x-axis-tracker.ts
Normal file
111
packages/plugin-outline-pane/src/helper/x-axis-tracker.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* X 轴追踪器,左右移动光标时获取正确位置
|
||||
*/
|
||||
import { INode, INodeParent, isRootNode } from '../../../../document/node';
|
||||
import Location, {
|
||||
isLocationChildrenDetail,
|
||||
LocationChildrenDetail,
|
||||
LocationData,
|
||||
LocationDetailType,
|
||||
} from '../../../../document/location';
|
||||
import { LocateEvent } from '../../../../globals';
|
||||
import { isContainer } from './is-container';
|
||||
|
||||
export default class XAxisTracker {
|
||||
private location!: Location;
|
||||
private start: number = 0;
|
||||
|
||||
/**
|
||||
* @param unit 移动单位
|
||||
*/
|
||||
constructor(readonly unit = 15) {}
|
||||
|
||||
track(loc: Location, e: LocateEvent): LocationData | null {
|
||||
this.location = loc;
|
||||
|
||||
if (this.start === 0) {
|
||||
this.start = e.globalX;
|
||||
}
|
||||
|
||||
const parent = this.locate(e);
|
||||
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
target: parent as INodeParent,
|
||||
detail: {
|
||||
type: LocationDetailType.Children,
|
||||
index: parent.children.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位
|
||||
*/
|
||||
locate(e: LocateEvent): INode | null {
|
||||
if (!isLocationChildrenDetail(this.location.detail)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const delta = e.globalX - this.start;
|
||||
let direction = null;
|
||||
|
||||
if (delta < 0) {
|
||||
direction = 'left';
|
||||
} else {
|
||||
direction = 'right';
|
||||
}
|
||||
|
||||
const n = Math.floor(Math.abs(delta) / this.unit);
|
||||
|
||||
// console.log('x', e.globalX, 'y', e.globalY, 'delta', delta, 'n', n, 'start', this.start);
|
||||
|
||||
if (n < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 一旦移动一个单位,就将"原点"清零
|
||||
this.reset();
|
||||
|
||||
const node = this.location.target;
|
||||
const index = (this.location.detail as LocationChildrenDetail).index;
|
||||
let parent = null;
|
||||
|
||||
if (direction === 'left') {
|
||||
// 如果光标是往左运动
|
||||
// 该节点如果不是最后一个节点,那么就没有继续查找下去的必要
|
||||
// console.log('>>> [left]', index, node.children.length, node);
|
||||
if (isRootNode(node)) {
|
||||
return null;
|
||||
}
|
||||
// index 为 0 表示第一个位置
|
||||
// 第一个位置或者不是最后以为位置,都不需要处理
|
||||
if (index < node.children.length - 1) {
|
||||
return null;
|
||||
}
|
||||
parent = node.parent as INode;
|
||||
} else {
|
||||
// 插入线一般是在元素下面,所以这边需要多减去 1,即 -2
|
||||
if (index === 0) {
|
||||
return null;
|
||||
}
|
||||
const i2 = Math.max(index - 1, 0);
|
||||
parent = node.children[i2];
|
||||
// console.log('>>> [right]', index, i2, parent, node.id);
|
||||
}
|
||||
|
||||
// parent 节点判断
|
||||
if (!parent || !isContainer(parent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.start = 0;
|
||||
}
|
||||
}
|
||||
1
packages/plugin-outline-pane/src/icons/border-outer.svg
Normal file
1
packages/plugin-outline-pane/src/icons/border-outer.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677547122" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1131" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1020.14942288-40.33632922H3.85057712c-24.44088257 0-44.18690634 19.74602377-44.18690634 44.18690634v1016.29884576c0 24.44088257 19.74602377 44.18690634 44.18690634 44.18690634h1016.29884576c24.44088257 0 44.18690634-19.74602377 44.18690634-44.18690634V3.85057712c0-24.44088257-19.74602377-44.18690634-44.18690634-44.18690634z m-55.23363292 1005.25211918H59.08421004V59.08421004h905.83157992v905.83157992z" fill="#ffffff" p-id="1132"></path><path d="M473.33645695 310.39723984h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672659v-77.32708608c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.32708608c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672659zM222.02342717 561.71026963h77.32708608c6.07569962 0 11.04672658-4.97102697 11.04672659-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672659-11.04672658h-77.32708608c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658zM724.64948675 561.71026963h77.32708608c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.32708608c-6.07569962 0-11.04672658 4.97102697-11.04672659 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672659 11.04672658zM473.33645695 561.71026963h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.3270861c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672658h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672658v77.3270861c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658zM473.33645695 813.02329941h77.3270861c6.07569962 0 11.04672658-4.97102697 11.04672658-11.04672658v-77.32708608c0-6.07569962-4.97102697-11.04672658-11.04672658-11.04672659h-77.3270861c-6.07569962 0-11.04672658 4.97102697-11.04672658 11.04672659v77.32708608c0 6.07569962 4.97102697 11.04672658 11.04672658 11.04672658z" fill="#ffffff" p-id="1133"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
packages/plugin-outline-pane/src/icons/caret-down.svg
Normal file
1
packages/plugin-outline-pane/src/icons/caret-down.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677537258" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="907" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M965.46812628 219.26174551H58.53187372c-27.20256421 0-42.39181327 28.72148912-25.54555523 48.3294288l453.46812628 525.82418542c12.97990373 15.05116498 37.97312263 15.05116498 51.09111046 0L991.01368151 267.59117431c16.84625804-19.6079397 1.65700898-48.3294288-25.54555523-48.3294288z" fill="#ffffff" p-id="908"></path></svg>
|
||||
|
After Width: | Height: | Size: 700 B |
1
packages/plugin-outline-pane/src/icons/caret-right.svg
Normal file
1
packages/plugin-outline-pane/src/icons/caret-right.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1562677542938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1019" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M793.41535973 486.45444477L267.59117431 32.98631849c-19.6079397-16.84625804-48.3294288-1.65700898-48.3294288 25.54555523v906.93625256c0 27.20256421 28.72148912 42.39181327 48.3294288 25.54555523l525.82418542-453.46812628c15.05116498-12.97990373 15.05116498-38.11120672 0-51.09111046z" fill="#ffffff" p-id="1020"></path></svg>
|
||||
|
After Width: | Height: | Size: 702 B |
10
packages/plugin-outline-pane/src/index.ts
Normal file
10
packages/plugin-outline-pane/src/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import Pane from './views/pane';
|
||||
|
||||
export default {
|
||||
name: 'outline-tree',
|
||||
title: {
|
||||
label: '大纲树',
|
||||
icon: { name: 'outline', size: '14px' },
|
||||
},
|
||||
content: Pane,
|
||||
};
|
||||
4
packages/plugin-outline-pane/src/locale/en-US.json
Normal file
4
packages/plugin-outline-pane/src/locale/en-US.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Designer not found": "Designer not found",
|
||||
"No opened document": "No opened document",
|
||||
}
|
||||
10
packages/plugin-outline-pane/src/locale/index.ts
Normal file
10
packages/plugin-outline-pane/src/locale/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { createIntl } from '../../../globals';
|
||||
import en_US from './en-US.json';
|
||||
import zh_CN from './zh-CN.json';
|
||||
|
||||
const { intl, getLocale, setLocale } = createIntl({
|
||||
'en-US': en_US,
|
||||
'zh-CN': zh_CN,
|
||||
});
|
||||
|
||||
export { intl, getLocale, setLocale };
|
||||
4
packages/plugin-outline-pane/src/locale/zh-CN.json
Normal file
4
packages/plugin-outline-pane/src/locale/zh-CN.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Designer not found": "未发现设计器模块",
|
||||
"No opened document": "没有打开的文档"
|
||||
}
|
||||
101
packages/plugin-outline-pane/src/main.ts
Normal file
101
packages/plugin-outline-pane/src/main.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { computed, obx } from '../../globals';
|
||||
import Designer from '../../designer/src/designer/designer';
|
||||
import { ISensor, LocateEvent } from '../../designer/src/designer/helper/dragon';
|
||||
import { Tree } from './tree';
|
||||
import Location from '../../designer/src/designer/helper/location';
|
||||
|
||||
class TreeMaster {
|
||||
constructor(readonly designer: Designer) {}
|
||||
|
||||
private treeMap = new Map<string, Tree>();
|
||||
@computed get currentTree(): Tree | null {
|
||||
const doc = this.designer?.currentDocument;
|
||||
if (doc) {
|
||||
const id = doc.id;
|
||||
if (this.treeMap.has(id)) {
|
||||
return this.treeMap.get(id)!;
|
||||
}
|
||||
const tree = new Tree(doc);
|
||||
// TODO: listen purge event to remove
|
||||
this.treeMap.set(id, tree);
|
||||
return tree;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mastersMap = new Map<Designer, TreeMaster>();
|
||||
function getTreeMaster(designer: Designer): TreeMaster {
|
||||
let master = mastersMap.get(designer);
|
||||
if (!master) {
|
||||
master = new TreeMaster(designer);
|
||||
mastersMap.set(designer, master);
|
||||
}
|
||||
return master;
|
||||
}
|
||||
|
||||
export class OutlineMain implements ISensor {
|
||||
private _designer?: Designer;
|
||||
@obx.ref private _master?: TreeMaster;
|
||||
get master() {
|
||||
return this._master;
|
||||
}
|
||||
|
||||
constructor(readonly editor: any) {
|
||||
if (editor.designer) {
|
||||
this.setupDesigner(editor.designer);
|
||||
} else {
|
||||
editor.once('designer.ready', (designer: Designer) => {
|
||||
this.setupDesigner(designer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fixEvent(e: LocateEvent): LocateEvent {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
locate(e: LocateEvent): Location | undefined {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
isEnter(e: LocateEvent): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
deactiveSensor(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
private setupDesigner(designer: Designer) {
|
||||
this._designer = designer;
|
||||
this._master = getTreeMaster(designer);
|
||||
designer.dragon.addSensor(this);
|
||||
}
|
||||
|
||||
purge() {
|
||||
this._designer?.dragon.removeSensor(this);
|
||||
// todo purge treeMaster if needed
|
||||
}
|
||||
|
||||
private _sensorAvailable: boolean = false;
|
||||
/**
|
||||
* @see ISensor
|
||||
*/
|
||||
get sensorAvailable() {
|
||||
return this._sensorAvailable;
|
||||
}
|
||||
|
||||
private _shell: HTMLDivElement | null = null;
|
||||
mount(shell: HTMLDivElement | null) {
|
||||
if (this._shell === shell) {
|
||||
return;
|
||||
}
|
||||
this._shell = shell;
|
||||
if (shell) {
|
||||
this._sensorAvailable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
220
packages/plugin-outline-pane/src/sensor.ts
Normal file
220
packages/plugin-outline-pane/src/sensor.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { ISenseAble, LocateEvent, isNodesDragTarget, activeTracker, getCurrentDocument } from '../../../globals';
|
||||
import Location, { isLocationChildrenDetail, LocationDetailType } from '../../../document/location';
|
||||
import tree from './tree';
|
||||
import Scroller, { ScrollTarget } from '../../../document/scroller';
|
||||
import { isShadowNode } from '../../../document/node/shadow-node';
|
||||
import TreeNode from './tree-node';
|
||||
import { INodeParent } from '../../../document/node';
|
||||
import DwellTimer from './helper/dwell-timer';
|
||||
import XAxisTracker from './helper/x-axis-tracker';
|
||||
|
||||
export const OutlineBoardID = 'outline-board';
|
||||
export default class OutlineBoard implements ISenseAble {
|
||||
id = OutlineBoardID;
|
||||
|
||||
get bounds() {
|
||||
const rootElement = this.element;
|
||||
const clientBound = rootElement.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
height: clientBound.height,
|
||||
width: clientBound.width,
|
||||
top: clientBound.top,
|
||||
left: clientBound.left,
|
||||
right: clientBound.right,
|
||||
bottom: clientBound.bottom,
|
||||
scale: 1,
|
||||
scrollHeight: rootElement.scrollHeight,
|
||||
scrollWidth: rootElement.scrollWidth,
|
||||
};
|
||||
}
|
||||
|
||||
sensitive: boolean = true;
|
||||
private sensing: boolean = false;
|
||||
|
||||
private scrollTarget = new ScrollTarget(this.element);
|
||||
private scroller = new Scroller(this, this.scrollTarget);
|
||||
|
||||
constructor(readonly element: HTMLDivElement) {
|
||||
activeTracker.onChange(({ node, detail }) => {
|
||||
const treeNode = isShadowNode(node) ? tree.getTreeNode(node.origin) : tree.getTreeNode(node);
|
||||
if (treeNode.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail && detail.type === LocationDetailType.Children) {
|
||||
treeNode.expand(true);
|
||||
} else {
|
||||
treeNode.expandParents();
|
||||
}
|
||||
this.scrollToNode(treeNode, detail);
|
||||
});
|
||||
}
|
||||
|
||||
private tryScrollAgain: number | null = null;
|
||||
scrollToNode(treeNode: TreeNode, detail?: any, tryTimes: number = 0) {
|
||||
this.tryScrollAgain = null;
|
||||
if (this.sensing) {
|
||||
// is a active sensor
|
||||
return;
|
||||
}
|
||||
|
||||
const opt: any = {};
|
||||
let scroll = false;
|
||||
let rect: ClientRect | null;
|
||||
if (detail && isLocationChildrenDetail(detail)) {
|
||||
rect = tree.getInsertionRect();
|
||||
} else {
|
||||
rect = treeNode.computeRect();
|
||||
}
|
||||
|
||||
if (!rect) {
|
||||
if (!this.tryScrollAgain && tryTimes < 3) {
|
||||
this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(treeNode, detail, tryTimes + 1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const scrollTarget = this.scrollTarget;
|
||||
const st = scrollTarget.top;
|
||||
const { height, top, bottom, scrollHeight } = this.bounds;
|
||||
|
||||
if (rect.top < top || rect.bottom > bottom) {
|
||||
opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height);
|
||||
scroll = true;
|
||||
}
|
||||
|
||||
if (scroll && this.scroller) {
|
||||
this.scroller.scrollTo(opt);
|
||||
}
|
||||
}
|
||||
|
||||
isEnter(e: LocateEvent): boolean {
|
||||
return this.inRange(e);
|
||||
}
|
||||
|
||||
inRange(e: LocateEvent): boolean {
|
||||
const rect = this.bounds;
|
||||
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
|
||||
}
|
||||
|
||||
deactive(): void {
|
||||
this.sensing = false;
|
||||
console.log('>>> deactive');
|
||||
}
|
||||
|
||||
fixEvent(e: LocateEvent): LocateEvent {
|
||||
return e;
|
||||
}
|
||||
|
||||
private dwellTimer: DwellTimer = new DwellTimer(450);
|
||||
private xAxisTracker = new XAxisTracker();
|
||||
|
||||
locate(e: LocateEvent): Location | undefined {
|
||||
this.sensing = true;
|
||||
this.scroller.scrolling(e);
|
||||
|
||||
const dragTarget = e.dragTarget;
|
||||
// FIXME: not support multiples/nodedatas/any data,
|
||||
const dragment = isNodesDragTarget(dragTarget) ? dragTarget.nodes[0] : null;
|
||||
if (!dragment) {
|
||||
return;
|
||||
}
|
||||
const doc = getCurrentDocument()!;
|
||||
const preDraggedNode = doc.dropLocation && doc.dropLocation.target;
|
||||
|
||||
// 左右移动追踪,一旦左右移动满足位置条件,直接返回即可。
|
||||
if (doc.dropLocation) {
|
||||
const loc2 = this.xAxisTracker.track(doc.dropLocation, e);
|
||||
if (loc2) {
|
||||
this.dwellTimer.end();
|
||||
return doc.createLocation(loc2);
|
||||
}
|
||||
} else {
|
||||
this.dwellTimer.end();
|
||||
return doc.createLocation({
|
||||
target: dragment.parent!,
|
||||
detail: {
|
||||
type: LocationDetailType.Children,
|
||||
index: dragment.index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 这语句的后半段是解决"丢帧"问题
|
||||
// e 有一种情况,是从 root > .flow 开始冒泡,而不是实际节点。这种情况往往发生在:光标在插入框内移动
|
||||
// 此时取上一次插入位置的 node 即可
|
||||
const treeNode = tree.getTreeNodeByEvent(e as any) || (preDraggedNode && tree.getTreeNode(preDraggedNode));
|
||||
|
||||
// TODO: 没有判断是否可以放入 isDropContainer,决定 target 的值是父节点还是本节点
|
||||
if (!treeNode || dragment === treeNode.node || treeNode.ignored) {
|
||||
this.dwellTimer.end();
|
||||
console.warn('not found tree-node or other reasons', treeNode, e);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// console.log('I am at', treeNode.id, e);
|
||||
|
||||
const rect = treeNode.computeRect();
|
||||
if (!rect) {
|
||||
this.dwellTimer.end();
|
||||
console.warn('can not get the rect, node', treeNode.id);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const node = treeNode.node;
|
||||
const parentNode = node.parent;
|
||||
|
||||
if (!parentNode) {
|
||||
this.dwellTimer.end();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let index = Math.max(parentNode.children.indexOf(node), 0);
|
||||
const center = rect.top + rect.height / 2;
|
||||
|
||||
// 常规处理
|
||||
// 如果可以展开,但是没有展开,需要设置延时器,检查停留时间然后展开
|
||||
// 最后返回合适的位置信息
|
||||
// FIXME: 容器判断存在问题,比如 img 是可以被放入的
|
||||
if (treeNode.isContainer() && !treeNode.expanded) {
|
||||
if (e.globalY > center) {
|
||||
this.dwellTimer.start(treeNode.id, () => {
|
||||
doc.createLocation({
|
||||
target: node as INodeParent,
|
||||
detail: {
|
||||
type: LocationDetailType.Children,
|
||||
index: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.dwellTimer.end();
|
||||
}
|
||||
|
||||
// 如果节点是展开状态,并且光标是在其下方,不做任何处理,直接返回即可
|
||||
// 如果不做这个处理,那么会出现"抖动"情况:在当前元素中心线下方时,会作为该元素的第一个子节点插入,而又会碰到已经存在对第一个字节点"争相"处理
|
||||
if (treeNode.expanded) {
|
||||
if (e.globalY > center) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果光标移动到节点中心线下方,则将元素插入到该节点下方
|
||||
// 反之插入该节点上方
|
||||
if (e.globalY > center) {
|
||||
// down
|
||||
index = index + 1;
|
||||
}
|
||||
|
||||
index = Math.min(index, parentNode.children.length);
|
||||
|
||||
return doc.createLocation({
|
||||
target: parentNode,
|
||||
detail: {
|
||||
type: LocationDetailType.Children,
|
||||
index,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
233
packages/plugin-outline-pane/src/tree-node.ts
Normal file
233
packages/plugin-outline-pane/src/tree-node.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { computed, obx, TitleContent } from '../../globals';
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
import DocumentModel from '../../designer/src/designer/document/document-model';
|
||||
import { isLocationChildrenDetail } from '../../designer/src/designer/helper/location';
|
||||
import Designer from '../../designer/src/designer/designer';
|
||||
import { Tree } from './tree';
|
||||
|
||||
export interface Title {
|
||||
label: string;
|
||||
icon?: string;
|
||||
actions?: any;
|
||||
}
|
||||
|
||||
export default class TreeNode {
|
||||
get id(): string {
|
||||
return this.node.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以展开
|
||||
*/
|
||||
@computed get expandable(): boolean {
|
||||
return this.hasChildren() || this.isSlotContainer() || this.dropIndex != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入"线"位置信息
|
||||
*/
|
||||
@computed get dropIndex(): number | null {
|
||||
const loc = this.node.document.dropLocation;
|
||||
return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail.index : null;
|
||||
}
|
||||
|
||||
@computed get depth(): number {
|
||||
return this.node.zLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是响应投放区
|
||||
*/
|
||||
@computed isResponseDropping(): boolean {
|
||||
const loc = this.node.document.dropLocation;
|
||||
if (!loc) {
|
||||
return false;
|
||||
}
|
||||
return loc.target === this.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认为折叠状态
|
||||
* 在初始化根节点时,设置为展开状态
|
||||
*/
|
||||
@obx.ref private _expanded = false;
|
||||
get expanded(): boolean {
|
||||
return this.expandable && this._expanded;
|
||||
}
|
||||
|
||||
set expanded(value: boolean) {
|
||||
this._expanded = value;
|
||||
}
|
||||
|
||||
@computed get hovering() {
|
||||
return this.designer.hovering.current === this.node;
|
||||
}
|
||||
|
||||
@computed get hidden(): boolean {
|
||||
return this.node.getExtraProp('hidden', false)?.getValue() === true;
|
||||
}
|
||||
|
||||
@computed get ignored(): boolean {
|
||||
return this.node.getExtraProp('ignored', false)?.getValue() === true;
|
||||
}
|
||||
|
||||
@computed get locked(): boolean {
|
||||
return this.node.getExtraProp('locked', false)?.getValue() === true;
|
||||
}
|
||||
|
||||
@computed get selected(): boolean {
|
||||
// TODO: check is dragging
|
||||
const selection = this.document.selection;
|
||||
return selection.has(this.node.id);
|
||||
}
|
||||
|
||||
@computed get title(): TitleContent {
|
||||
return this.node.title;
|
||||
}
|
||||
|
||||
@computed get parent() {
|
||||
const parent = this.node.parent;
|
||||
if (parent) {
|
||||
return this.tree.getTreeNode(parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@computed get slots(): TreeNode[] {
|
||||
// todo: shallowEqual
|
||||
return this.node.slots.map((node) => this.tree.getTreeNode(node));
|
||||
}
|
||||
|
||||
@computed get children(): TreeNode[] | null {
|
||||
return this.node.children?.map((node) => this.tree.getTreeNode(node)) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是容器,允许子节点拖入
|
||||
*/
|
||||
isContainer(): boolean {
|
||||
return this.node.isContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有"插槽"
|
||||
*/
|
||||
isSlotContainer(): boolean {
|
||||
return this.node.isSlotContainer();
|
||||
}
|
||||
|
||||
hasChildren(): boolean {
|
||||
return this.isContainer() && this.node.children?.notEmpty() ? true : false;
|
||||
}
|
||||
|
||||
/*
|
||||
get xForValue() {
|
||||
const node = this.node;
|
||||
return isElementNode(node) && node.xforValue ? node.xforValue : null;
|
||||
}
|
||||
|
||||
get flowHidden() {
|
||||
return (this.node as ElementNode).flowHidden;
|
||||
}
|
||||
|
||||
get flowIndex() {
|
||||
return (this.node as ElementNode).flowIndex;
|
||||
}
|
||||
|
||||
get conditionFlow() {
|
||||
return (this.node as ElementNode).conditionFlow;
|
||||
}
|
||||
|
||||
hasXIf() {
|
||||
return hasConditionFlow(this.node);
|
||||
}
|
||||
|
||||
hasXFor() {
|
||||
const node = this.node;
|
||||
return isElementNode(node) && node.xforFn;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 展开节点,支持依次展开父节点
|
||||
*/
|
||||
expand(tryExpandParents: boolean = false) {
|
||||
// 这边不能直接使用 expanded,需要额外判断是否可以展开
|
||||
// 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开
|
||||
if (this.expandable && !this._expanded) {
|
||||
this.expanded = true;
|
||||
}
|
||||
if (tryExpandParents) {
|
||||
this.expandParents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 光标停留处理
|
||||
* 超过一定时间,展开节点
|
||||
*/
|
||||
private dwellTimer: number | undefined;
|
||||
clearDwellTimer() {
|
||||
clearTimeout(this.dwellTimer);
|
||||
this.dwellTimer = undefined;
|
||||
}
|
||||
willExpand() {
|
||||
if (this.dwellTimer) {
|
||||
return;
|
||||
}
|
||||
this.clearDwellTimer();
|
||||
if (this.expanded) {
|
||||
return;
|
||||
}
|
||||
this.dwellTimer = setTimeout(() => {
|
||||
this.clearDwellTimer();
|
||||
this.expand(true);
|
||||
}, 400) as any;
|
||||
}
|
||||
|
||||
expandParents() {
|
||||
let p = this.node.parent;
|
||||
while (p) {
|
||||
this.tree.getTreeNode(p).expanded = true;
|
||||
p = p.parent;
|
||||
}
|
||||
}
|
||||
|
||||
private titleRef: HTMLDivElement | null = null;
|
||||
mount(ref: HTMLDivElement | null) {
|
||||
this.titleRef = ref;
|
||||
}
|
||||
|
||||
computeRect() {
|
||||
let target = this.titleRef;
|
||||
if (!target) {
|
||||
const nodeId = this.id;
|
||||
target = window.document.querySelector(`div[data-id="${nodeId}"]`);
|
||||
}
|
||||
return target && target.getBoundingClientRect();
|
||||
}
|
||||
|
||||
select(isMulti: boolean) {
|
||||
const node = this.node;
|
||||
|
||||
/*
|
||||
if (this.hasXIf()) {
|
||||
(node as ElementNode).setFlowVisible();
|
||||
}
|
||||
*/
|
||||
|
||||
const selection = node.document.selection;
|
||||
if (isMulti) {
|
||||
selection.add(node.id);
|
||||
} else {
|
||||
selection.select(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
readonly designer: Designer;
|
||||
readonly document: DocumentModel;
|
||||
constructor(readonly tree: Tree, readonly node: Node) {
|
||||
this.document = node.document;
|
||||
this.designer = this.document.designer;
|
||||
}
|
||||
}
|
||||
23
packages/plugin-outline-pane/src/tree.ts
Normal file
23
packages/plugin-outline-pane/src/tree.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import TreeNode from './tree-node';
|
||||
import DocumentModel from '../../designer/src/designer/document/document-model';
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
|
||||
export class Tree {
|
||||
private treeNodesMap = new Map<string, TreeNode>();
|
||||
|
||||
readonly root: TreeNode;
|
||||
|
||||
constructor(readonly document: DocumentModel) {
|
||||
this.root = this.getTreeNode(document.rootNode);
|
||||
}
|
||||
|
||||
getTreeNode(node: Node): TreeNode {
|
||||
if (this.treeNodesMap.has(node.id)) {
|
||||
return this.treeNodesMap.get(node.id)!;
|
||||
}
|
||||
|
||||
const treeNode = new TreeNode(this, node);
|
||||
this.treeNodesMap.set(node.id, treeNode);
|
||||
return treeNode;
|
||||
}
|
||||
}
|
||||
251
packages/plugin-outline-pane/src/views/pane.less
Normal file
251
packages/plugin-outline-pane/src/views/pane.less
Normal file
@ -0,0 +1,251 @@
|
||||
/* 面板背景的颜色 */
|
||||
@pane-bgcolor: #333131; // #1a1c23;
|
||||
/* 标题背景色 */
|
||||
//@title-bgcolor: var(--pane-bg-color; // backup rgba(0, 0, 0, 0.2);
|
||||
/* 标题边框色 */
|
||||
@title-bdcolor: transparent;
|
||||
@title-selectedcolor: #111;
|
||||
@section-bgcolor: transparent;
|
||||
@section-bdcolor: rgba(0, 0, 0, 0.1);
|
||||
/* 文字颜色 */
|
||||
@text-color: #ffffff;
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.my-outline-pane {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
|
||||
> .tree-scroll-container {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.my-outline-tree {
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 5px;
|
||||
|
||||
// 禁用 Text Select
|
||||
user-select: none;
|
||||
|
||||
.insertion {
|
||||
pointer-events: none !important;
|
||||
border: 1px dashed var(--color-brand-light);
|
||||
height: 25px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.condition-flow-container {
|
||||
@bd-setting: 1px solid #7b605b;
|
||||
border-top: @bd-setting;
|
||||
border-bottom: @bd-setting;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
border-left: @bd-setting;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: ' ';
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
color: rgb(217, 217, 217);
|
||||
|
||||
.c-control-flow-title {
|
||||
text-align: center;
|
||||
background-color: #7b605b;
|
||||
height: 14px;
|
||||
> b {
|
||||
transform: scale(0.75);
|
||||
transform-origin: top;
|
||||
text-shadow: 0px 0px 2px black;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node-collapsed-icon {
|
||||
transition: transform 0.01s;
|
||||
margin-left: 4px;
|
||||
filter: opacity(0.5);
|
||||
|
||||
& > svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
|
||||
& > svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node-ignored-icon {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.tree-node-title {
|
||||
font-size: var(--font-size-text);
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: var(--pane-bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
|
||||
.tree-node-title-inner {
|
||||
flex: 1;
|
||||
height: 26px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tree-node-title-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background-color: var(--pane-bg-color);
|
||||
color: var(--color-pane-label);
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.tree-node-title-text {
|
||||
flex: 1;
|
||||
color: rgb(217, 217, 217);
|
||||
// problem
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.x-for-text {
|
||||
color: #9370db;
|
||||
}
|
||||
|
||||
&.x-if-text {
|
||||
color: #ff6308;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.editable {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
> .tree-node-title > .tree-node-collapsed-icon {
|
||||
transform-origin: center;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
//> .branches {
|
||||
// > .tree-node > .tree-node-title > .tree-node-icon {
|
||||
// margin-left: 9px;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
&.hover {
|
||||
& > .tree-node-title {
|
||||
background: @title-selectedcolor * 1.6;
|
||||
|
||||
.tree-node-ignored-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 忽略节点处理
|
||||
&.ignored {
|
||||
.tree-node-title-text {
|
||||
color: #9b9b9b;
|
||||
}
|
||||
|
||||
.tree-node-collapsed-icon,
|
||||
.tree-node-ignored-icon {
|
||||
display: block;
|
||||
filter: opacity(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 选中节点处理
|
||||
&.selected {
|
||||
& > .tree-node-title {
|
||||
background: @title-selectedcolor;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理拖入节点
|
||||
&.dropping {
|
||||
& > .tree-node-title {
|
||||
background: @title-selectedcolor * 0.75;
|
||||
}
|
||||
|
||||
& > .branches:before {
|
||||
border-left: 1px solid var(--color-brand-light);
|
||||
}
|
||||
}
|
||||
|
||||
.branches {
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
border-left: 0.5px solid rgba(149, 216, 160, 0.25);
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 10px;
|
||||
content: ' ';
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.x-flow {
|
||||
&:before {
|
||||
border-left: 1px solid #ff6308;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
packages/plugin-outline-pane/src/views/pane.tsx
Normal file
47
packages/plugin-outline-pane/src/views/pane.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
import { OutlineMain } from '../main';
|
||||
import { intl } from '../locale';
|
||||
import { observer } from '../../../globals/src';
|
||||
import './style.less';
|
||||
|
||||
@observer
|
||||
export default class OutlinePane extends Component<{ editor: any }> {
|
||||
private main = new OutlineMain(this.props.editor);
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.main.purge();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.main.master) {
|
||||
return (
|
||||
<div className="lc-outline-pane">
|
||||
<p className="lc-outline-notice">{intl('Designer not found')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.main.master.currentTree) {
|
||||
return (
|
||||
<div className="lc-outline-pane">
|
||||
<p className="lc-outline-notice">{intl('No opened document')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-outline-pane">
|
||||
<div
|
||||
ref={shell => this.main.mount(shell)}
|
||||
className="lc-outline-tree-container"
|
||||
>
|
||||
<TreeView tree={this.main.master.currentTree} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
56
packages/plugin-outline-pane/src/views/tree-branches.tsx
Normal file
56
packages/plugin-outline-pane/src/views/tree-branches.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { observer } from '../../../globals/src';
|
||||
|
||||
@observer
|
||||
export default class TreeBranches extends Component<TreeNodeProps> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const treeNode = this.props.treeNode;
|
||||
const { expanded } = treeNode;
|
||||
|
||||
if (!expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const branchClassName = classNames({
|
||||
branches: !isRootNode(treeNode.node),
|
||||
// 'x-branch': treeNode.hasXIf() && treeNode.branchIndex !== treeNode.branchNode!.children.length - 1,
|
||||
});
|
||||
|
||||
let children: any = [];
|
||||
|
||||
if (treeNode.hasChildren() /* || node.hasSlots() */) {
|
||||
children = treeNode.children.map((child: TreeNode) => {
|
||||
if (child.hasXIf()) {
|
||||
if (child.flowIndex === 0) {
|
||||
const conditionFlowContainer = classNames('condition-group-container', {
|
||||
hidden: child.hidden,
|
||||
});
|
||||
return (
|
||||
<div key={child.id} className={conditionFlowContainer} data-id={child.id}>
|
||||
<div className="c-control-flow-title"><b>Condition Flow</b></div>
|
||||
{child.conditionGroup!.children.map(c => {
|
||||
return <TreeNodeView key={c.id} treeNode={tree.getTreeNode(c)} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return <TreeNodeView key={child.id} treeNode={child} />;
|
||||
});
|
||||
}
|
||||
if (treeNode.dropIndex != null) {
|
||||
children.splice(
|
||||
treeNode.dropIndex,
|
||||
0,
|
||||
<div key="insertion" ref={ref => tree.mountInsertion(ref)} className="insertion" />,
|
||||
);
|
||||
}
|
||||
|
||||
return children.length > 0 && <div className={branchClassName}>{children}</div>;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import DIVIcon from 'my-icons/container.svg';
|
||||
import IMGIcon from 'my-icons/image.svg';
|
||||
import { observer } from '@ali/recore';
|
||||
|
||||
@observer
|
||||
export default class TreeNodeIconView extends React.Component<{ tagName: string }> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tagName } = this.props;
|
||||
switch (tagName) {
|
||||
case 'img': {
|
||||
console.log('>>> tag:', tagName);
|
||||
return <IMGIcon />;
|
||||
}
|
||||
default:
|
||||
return <DIVIcon />;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/plugin-outline-pane/src/views/tree-node.tsx
Normal file
58
packages/plugin-outline-pane/src/views/tree-node.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
export interface TreeNodeProps {
|
||||
treeNode: TreeNode;
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class TreeNodeView extends Component<TreeNodeProps> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { treeNode } = this.props;
|
||||
const className = classNames('tree-node', {
|
||||
// 是否展开
|
||||
expanded: treeNode.expanded,
|
||||
// 是否悬停
|
||||
hover: treeNode.hover,
|
||||
// 是否选中
|
||||
selected: treeNode.selected,
|
||||
// 是否隐藏
|
||||
hidden: treeNode.hidden,
|
||||
// 是否忽略的
|
||||
ignored: treeNode.ignored,
|
||||
// 是否锁定的
|
||||
locked: treeNode.locked,
|
||||
// 是否投放响应
|
||||
dropping: treeNode.dropIndex != null,
|
||||
// 是否?
|
||||
highlight: treeNode.isDropContainer() && treeNode.dropIndex == null,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className} data-id={treeNode.id}>
|
||||
<TreeTitle treeNode={treeNode} />
|
||||
<TreeBranches treeNode={treeNode} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function findTargetByEvent(e: MouseEvent): HTMLElement | null {
|
||||
return (e.target as HTMLElement).closest('.tree-node') as HTMLElement;
|
||||
}
|
||||
|
||||
export function getNodeIDFromTarget(target: HTMLElement): string | null {
|
||||
return target.getAttribute('data-id');
|
||||
}
|
||||
|
||||
export function getNodeIDByEvent(e: MouseEvent): string | null {
|
||||
const target = findTargetByEvent(e);
|
||||
if (target) {
|
||||
return getNodeIDFromTarget(target);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
176
packages/plugin-outline-pane/src/views/tree-title.tsx
Normal file
176
packages/plugin-outline-pane/src/views/tree-title.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { observer } from '@ali/recore';
|
||||
import React, { Component, KeyboardEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import ElementNode from '../../../../document/node/element-node';
|
||||
import { isElementNode } from '../../../../document/node';
|
||||
import { TreeNodeProps } from './tree-node';
|
||||
import TreeNodeIconView from './tree-node-icon-view';
|
||||
import CollapsedIcon from '../icons/caret-right.svg';
|
||||
import EyeCloseIcon from 'my-icons/eye-close.svg';
|
||||
|
||||
interface IState {
|
||||
editing: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class TreeNodeTitle extends Component<TreeNodeProps, IState> {
|
||||
|
||||
private inputRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: TreeNodeProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleIgnored() {
|
||||
const treeNode = this.props.treeNode;
|
||||
const node = treeNode.node as ElementNode;
|
||||
if (treeNode.ignored) {
|
||||
node.getDirective('x-ignore').remove();
|
||||
} else {
|
||||
node.getDirective('x-ignore').value = true;
|
||||
}
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
const treeNode = this.props.treeNode;
|
||||
const { expanded } = treeNode;
|
||||
treeNode.expanded = !expanded;
|
||||
}
|
||||
|
||||
renderExpandIcon() {
|
||||
const node = this.props.treeNode;
|
||||
|
||||
if (!node.expandable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tree-node-collapsed-icon"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
this.toggleExpanded();
|
||||
}}
|
||||
>
|
||||
<CollapsedIcon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
setTitle(xtitle: string = '') {
|
||||
const { treeNode } = this.props;
|
||||
const node = treeNode.node as ElementNode;
|
||||
const title = node.getProp('x-title');
|
||||
if (xtitle && xtitle !== node.tagName) {
|
||||
title.code = `"${xtitle}"`;
|
||||
} else {
|
||||
title.remove();
|
||||
}
|
||||
}
|
||||
|
||||
enableEdit = () => {
|
||||
this.setState({
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
saveEdit = () => {
|
||||
const { current } = this.inputRef;
|
||||
|
||||
if (current) {
|
||||
this.setTitle(current.value);
|
||||
}
|
||||
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
handleKeyUp(e: KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.keyCode === 13) {
|
||||
this.saveEdit();
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.cancelEdit();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { current } = this.inputRef;
|
||||
if (current) {
|
||||
current.select();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { treeNode } = this.props;
|
||||
const { editing } = this.state;
|
||||
const { title } = treeNode;
|
||||
const depth = treeNode.depth;
|
||||
const indent = depth * 12;
|
||||
|
||||
const titleClassName = classNames('tree-node-title');
|
||||
const titleTextClassName = classNames('tree-node-title-text', {
|
||||
'x-if-text': treeNode.hasXIf(),
|
||||
'x-for-text': treeNode.hasXFor(),
|
||||
});
|
||||
const xForValue = treeNode.xForValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={titleClassName}
|
||||
ref={ref => treeNode.mount(ref)}
|
||||
style={{ paddingLeft: indent, marginLeft: -indent }}
|
||||
>
|
||||
{this.renderExpandIcon()}
|
||||
<div className="tree-node-icon">
|
||||
<TreeNodeIconView tagName={treeNode.node.tagName} />
|
||||
</div>
|
||||
<div className="tree-node-title-inner" onDoubleClick={this.enableEdit}>
|
||||
{
|
||||
editing ?
|
||||
<input
|
||||
className="tree-node-title-input"
|
||||
defaultValue={title.label}
|
||||
onBlur={this.saveEdit}
|
||||
onKeyUp={e => {this.handleKeyUp(e)}}
|
||||
ref={this.inputRef}
|
||||
/>
|
||||
:
|
||||
<div className={titleTextClassName}>
|
||||
{title.label}
|
||||
{xForValue && (
|
||||
<span className="info">
|
||||
(x <b>{xForValue.length}</b>)
|
||||
</span>
|
||||
)}
|
||||
{treeNode.hasXIf() && (
|
||||
<span className="info">
|
||||
<b>{treeNode.flowHidden ? '' : '(visible)'}</b>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="tree-node-ignored-icon">
|
||||
{isElementNode(treeNode.node) && !editing && (
|
||||
<EyeCloseIcon
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.toggleIgnored();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user