prepare outline

This commit is contained in:
kangwei 2020-03-23 01:14:12 +08:00
parent f0a023df6d
commit dc8fdae09d
124 changed files with 3005 additions and 1165 deletions

View File

@ -1 +1,4 @@
编排模块
simulator/renderer 发 CDN

View File

@ -1,5 +1,5 @@
{
"name": "lowcode-designer",
"name": "@ali/lowcode-designer",
"version": "0.9.0",
"description": "alibaba lowcode designer",
"main": "src/index.ts",

View File

@ -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';

View File

@ -1 +0,0 @@
export * from './auxiliary';

View File

@ -1,4 +1,4 @@
.lc-auxiliary {
.lc-bem-tools {
pointer-events: none;
position: absolute;
top: 0;

View File

@ -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}

View File

@ -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>
);

View File

@ -1,4 +1,4 @@
@scope: lc-outlines;
@scope: lc-borders;
.@{scope} {
box-sizing: border-box;

View File

@ -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>
);

View File

@ -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, {

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
});
}

View File

@ -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';

View File

@ -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;

View File

@ -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',

View File

@ -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();

View File

@ -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;
}

View File

@ -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 {
/**

View File

@ -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

View File

@ -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;

View File

@ -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;
/**
*

View File

@ -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';

View File

@ -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 (

View File

@ -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;
/**
*

View File

@ -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';
/**
*

View File

@ -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';

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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 }> {

View File

@ -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 {
// 通知标记删除,需要告知服务端
// 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁,
// 哪个删除就
}

View File

@ -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;
}

View File

@ -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%';

View File

@ -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';

View File

@ -1 +1,3 @@
shared globals
发 CDN

View File

@ -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"

View File

@ -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$');

View File

@ -1,5 +1,5 @@
import { TipConfig } from './embed-tip';
import { EventEmitter } from 'events';
import { TipConfig } from '../../types';
export interface TipOptions extends TipConfig {
target: HTMLElement;

View File

@ -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>
);
}

View File

@ -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>
);
}
}

View File

@ -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 {

View File

@ -0,0 +1,2 @@
export * from './setter';
export * from './transducer';

View 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);
}

View 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;
}

View File

@ -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';

View 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';

View 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';

View File

@ -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';

View File

@ -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';

View 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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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() {

View File

@ -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();
},

View File

@ -0,0 +1,4 @@
export * from '@recore/obx';
import { observer } from '@recore/obx-react';
export { observer };

View 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[];
}

View 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;
}

View 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 };
}

View 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;

View 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';

View 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[] };
}

View 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[];

View 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;
}

View 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));
}

View 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;

View 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;

View File

@ -0,0 +1,13 @@
import { NpmInfo } from './npm';
export type UtilsMap = Array<
| {
name: string;
type: 'npm';
content: NpmInfo;
}
| {
name: string;
type: '';
}
>;

View 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';
}

View File

@ -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} />;
}

View File

@ -0,0 +1,6 @@
.idea/
.vscode/
build/
.*
~*
node_modules

View File

@ -0,0 +1,3 @@
{
"extends": "./node_modules/@recore/config/.eslintrc"
}

View File

@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 120,
"trailingComma": "all"
}

View 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"
}

View File

@ -0,0 +1 @@
大纲树

View 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;
}
}
}

View 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;
}

View 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;
}
}

View 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

View 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

View 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

View File

@ -0,0 +1,10 @@
import Pane from './views/pane';
export default {
name: 'outline-tree',
title: {
label: '大纲树',
icon: { name: 'outline', size: '14px' },
},
content: Pane,
};

View File

@ -0,0 +1,4 @@
{
"Designer not found": "Designer not found",
"No opened document": "No opened document",
}

View 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 };

View File

@ -0,0 +1,4 @@
{
"Designer not found": "未发现设计器模块",
"No opened document": "没有打开的文档"
}

View 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;
}
}
}

View 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,
},
});
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}
}
}

View 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>
);
}
}

View 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>;
}
}

View File

@ -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 />;
}
}
}

View 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;
}

View 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