This commit is contained in:
kangwei 2020-02-14 02:32:12 +08:00
parent 8d9f72ca3e
commit 625cba310c
77 changed files with 6023 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Tab indentation
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

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

View File

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

40
packages/designer/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
node_modules/
coverage/
build/
dist/
.idea/
.vscode/
.theia/
.recore/
~*
package-lock.json
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store
.Trash*
*.swp
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

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

View File

@ -0,0 +1,40 @@
{
"name": "lowcode-designer",
"version": "0.9.0",
"description": "alibaba lowcode designer",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"@recore/obx": "^1.0.5",
"@types/medium-editor": "^5.0.3",
"classnames": "^2.2.6",
"react": "^16",
"react-dom": "^16.7.0"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
"@types/jest": "^24.0.16",
"@types/react": "^16",
"@types/react-dom": "^16",
"@recore/config": "^2.0.0",
"ts-jest": "^24.0.2",
"ts-node": "^8.0.1",
"eslint": "^6.5.1",
"husky": "^1.1.2",
"jest": "^23.4.1",
"lint-staged": "^7.1.2",
"tslib": "^1.9.3",
"typescript": "^3.1.3",
"prettier": "^1.18.2"
},
"lint-staged": {
"./src/**/*.{ts,tsx}": [
"eslint --fix",
"git add"
]
}
}

View File

@ -0,0 +1,29 @@
.my-ghost-group {
box-sizing: border-box;
position: fixed;
z-index: 99999;
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
background-color: rgba(0, 0, 0, 0.4);
opacity: 0.5;
.my-ghost {
.my-ghost-title {
text-align: center;
font-size: var(--font-size-text);
text-overflow: ellipsis;
color: var(--color-text-light);
white-space: nowrap;
overflow: hidden;
}
}
}
.dragging {
position: fixed;
z-index: 99999;
width: 100%;
box-shadow: 0 0 6px grey;
}

View File

@ -0,0 +1,105 @@
import { Component } from 'react';
import { observer, obx } from '@ali/recore';
import { dragon } from '../../globals/dragon';
import './ghost.less';
import { OutlineBoardID } from '../builtin-panes/outline-pane/outline-board';
// import { INode } from '../../document/node';
type offBinding = () => any;
@observer
export default class Ghost extends Component {
private dispose: offBinding[] = [];
@obx.ref private dragment: any = null;
@obx.ref private x = 0;
@obx.ref private y = 0;
componentWillMount() {
this.dispose = [
dragon.onDragstart(e => {
this.dragment = e.dragTarget;
this.x = e.clientX;
this.y = e.clientY;
}),
dragon.onDrag(e => {
this.x = e.clientX;
this.y = e.clientY;
}),
dragon.onDragend(() => {
this.dragment = null;
this.x = 0;
this.y = 0;
}),
];
}
shouldComponentUpdate() {
return false;
}
componentWillUnmount() {
if (this.dispose) {
this.dispose.forEach(off => off());
}
}
renderGhostGroup() {
const dragment = this.dragment;
if (Array.isArray(dragment)) {
return dragment.map((node: any, index: number) => {
const ghost = (
<div className="my-ghost" key={`ghost-${index}`}>
<div className="my-ghost-title">{node.tagName}</div>
</div>
);
return ghost;
});
} else {
return (
<div className="my-ghost">
<div className="my-ghost-title">{dragment.tagName}</div>
</div>
);
}
}
render() {
if (!this.dragment) {
return null;
}
// let x = this.x;
// let y = this.y;
// todo: 考虑多个图标、title、不同 sensor 区域的形态
if (dragon.activeSensor && dragon.activeSensor.id === OutlineBoardID) {
// const nodeId = (this.dragment as INode).id;
// const elt = document.querySelector(`[data-id="${nodeId}"`) as HTMLDivElement;
//
// if (elt) {
// // do something
// // const target = elt.cloneNode(true) as HTMLDivElement;
// console.log('>>> target', elt);
// elt.classList.remove('hidden');
// elt.classList.add('dragging');
// elt.style.transform = `translate(${this.x}px, ${this.y}px)`;
// }
//
// return null;
// x -= 30;
// y += 30;
}
return (
<div
className="my-ghost-group"
style={{
transform: `translate(${this.x}px, ${this.y}px)`,
}}
>
{this.renderGhostGroup()}
</div>
);
}
}

View File

@ -0,0 +1,59 @@
import MediumEditor from 'medium-editor';
import { computed, obx } from '@ali/recore';
import { current } from './current';
import ElementNode from '../document/node/element-node';
class EmbedEditor {
@obx container?: HTMLDivElement | null;
private _editor?: any;
@computed getEditor(): any | null {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
const win = current.document!.contentWindow;
const doc = current.document!.ownerDocument;
if (!win || !doc || !this.container) {
return null;
}
const rect = this.container.getBoundingClientRect();
this._editor = new MediumEditor([], {
contentWindow: win,
ownerDocument: doc,
toolbar: {
diffLeft: rect.left,
diffTop: rect.top - 10,
},
elementsContainer: this.container,
});
return this._editor;
}
@obx.ref editing?: [ElementNode, string, HTMLElement];
edit(node: ElementNode, prop: string, el: HTMLElement) {
const ed = this.getEditor();
if (!ed) {
return;
}
this.exitAndSave();
console.info(el);
this.editing = [node, prop, el];
ed.origElements = el;
ed.setup();
}
exitAndSave() {
this.editing = undefined;
// removeElements
// get content save to
}
mount(container?: HTMLDivElement | null) {
this.container = container;
}
}
export default new EmbedEditor();

View File

@ -0,0 +1,22 @@
辅助类
对齐线
插入指示 insertion 竖线 横线 插入块 禁止插入块
幽灵替身 ghost
聚焦编辑指示
插入指示 insertion 竖线 横线 插入块 禁止插入块
竖线:红色,绿色
横线:红色,绿色
插入块:透明绿色,透明红色
投放指示线
cover
轮廓服务
悬停指示线 xray mode?
选中指示线
投放指示线
透视线 x-ray

View File

@ -0,0 +1,20 @@
.my-auxiliary {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: visible;
z-index: 800;
.embed-editor-toolbar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
> * {
pointer-events: all;
}
}
}

View File

@ -0,0 +1,31 @@
import { observer } from '@ali/recore';
import { Component } from 'react';
import { getCurrentDocument } from '../../globals';
import './auxiliary.less';
import { EdgingView } from './edging';
import { InsertionView } from './insertion';
import { SelectingView } from './selecting';
import EmbedEditorToolbar from './embed-editor-toolbar';
@observer
export class AuxiliaryView extends Component {
shouldComponentUpdate() {
return false;
}
render() {
const doc = getCurrentDocument();
if (!doc || !doc.ready) {
return null;
}
const { scrollX, scrollY, scale } = doc.viewport;
return (
<div className="my-auxiliary" style={{ transform: `translate(${-scrollX * scale}px,${-scrollY * scale}px)` }}>
<EmbedEditorToolbar />
<EdgingView />
<InsertionView />
<SelectingView />
</div>
);
}
}

View File

@ -0,0 +1,13 @@
// outline
// insertion
/*
// 插入指示 insertion 竖线 横线 插入块 禁止插入块
线绿
线绿
绿
线
cover
*/

View File

@ -0,0 +1,39 @@
.my-edging {
box-sizing: border-box;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
border: 1px dashed var(--color-brand-light);
z-index: 1;
background: rgba(95, 240, 114, 0.04);
will-change: transform, width, height;
transition-property: transform, width, height;
transition-duration: 60ms;
transition-timing-function: linear;
overflow: visible;
>.title {
position: absolute;
color: var(--color-brand-light);
top: -20px;
left: 0;
font-weight: lighter;
}
&.x-shadow {
border-color: rgba(138, 93, 226, 0.8);
background: rgba(138, 93, 226, 0.04);
>.title {
color: rgba(138, 93, 226, 1.0);
}
}
&.x-flow {
border-color: rgba(255, 99, 8, 0.8);
background: rgba(255, 99, 8, 0.04);
>.title {
color: rgb(255, 99, 8);
}
}
}

View File

@ -0,0 +1,62 @@
import { observer } from '@ali/recore';
import { Component } from 'react';
import { edging } from '../../globals/edging';
import './edging.less';
import { hasConditionFlow } from '../../document/node';
import { isShadowNode } from '../../document/node/shadow-node';
import { isConditionFlow } from '../../document/node/condition-flow';
import { current } from '../../globals';
@observer
export class EdgingView extends Component {
shouldComponentUpdate() {
return false;
}
render() {
const node = edging.watching;
if (!node || !edging.enable || (current.selection && current.selection.has(node.id))) {
return null;
}
// TODO: think of multi rects
// TODO: findDOMNode cause a render bug
const rect = node.document.computeRect(node);
if (!rect) {
return null;
}
const { scale, scrollTarget } = node.document.viewport;
const sx = scrollTarget!.left;
const sy = scrollTarget!.top;
const style = {
width: rect.width * scale,
height: rect.height * scale,
transform: `translate(${(sx + rect.left) * scale}px, ${(sy + rect.top) * scale}px)`,
} as any;
let className = 'my-edging';
// handle x-for node
if (isShadowNode(node)) {
className += ' x-shadow';
}
// handle x-if/else-if/else node
if (isConditionFlow(node) || hasConditionFlow(node)) {
className += ' x-flow';
}
// TODO:
// 1. thinkof icon
// 2. thinkof top|bottom|inner space
return (
<div className={className} style={style}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a className="title">{(node as any).title || node.tagName}</a>
</div>
);
}
}

View File

@ -0,0 +1,12 @@
import { Component } from 'react';
import embedEditor from '../../globals/embed-editor';
export default class EmbedEditorToolbar extends Component {
shouldComponentUpdate() {
return false;
}
render() {
return <div className="embed-editor-toolbar" ref={shell => embedEditor.mount(shell)} />;
}
}

View File

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

View File

@ -0,0 +1,23 @@
.my-insertion {
position: absolute;
top: -1.5px;
left: 0;
z-index: 12;
pointer-events: none !important;
background-color: var(--color-brand-light);
height: 3px;
&.cover {
top: 0;
height: auto;
width: auto;
opacity: 0.3;
}
&.vertical {
top: 0;
left: -1.5px;
width: 3px;
height: auto;
}
}

View File

@ -0,0 +1,139 @@
import { Component } from 'react';
import { observer } from '@ali/recore';
import { getCurrentDocument } from '../../globals';
import './insertion.less';
import Location, { isLocationChildrenDetail, isVertical, LocationChildrenDetail, Rect } from '../../document/location';
import { isConditionFlow } from '../../document/node/condition-flow';
import { getChildAt, INodeParent } from '../../document/node';
import DocumentContext from '../../document/document-context';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function processPropDetail() {
// return { insertType: 'cover', coverEdge: ? };
}
interface InsertionData {
edge?: Rect;
insertType?: string;
vertical?: boolean;
nearRect?: Rect;
coverRect?: Rect;
}
/**
* (INode)
*/
function processChildrenDetail(
doc: DocumentContext,
target: INodeParent,
detail: LocationChildrenDetail,
): InsertionData {
const edge = doc.computeRect(target);
if (!edge) {
return {};
}
const ret: any = {
edge,
insertType: 'before',
};
if (isConditionFlow(target)) {
ret.insertType = 'cover';
ret.coverRect = edge;
return ret;
}
if (detail.near) {
const { node, pos, rect, align } = detail.near;
ret.nearRect = rect || doc.computeRect(node);
ret.vertical = align ? align === 'V' : isVertical(ret.nearRect);
ret.insertType = pos;
return ret;
}
// from outline-tree: has index, but no near
// TODO: think of shadowNode & ConditionFlow
const { index } = detail;
let nearNode = getChildAt(target, index);
if (!nearNode) {
// index = 0, eg. nochild,
nearNode = getChildAt(target, index > 0 ? index - 1 : 0);
if (!nearNode) {
ret.insertType = 'cover';
ret.coverRect = edge;
return ret;
}
ret.insertType = 'after';
}
if (nearNode) {
ret.nearRect = doc.computeRect(nearNode);
ret.vertical = isVertical(ret.nearRect);
}
return ret;
}
/**
* detail "坐标"
*/
function processDetail({ target, detail, document }: Location): InsertionData {
if (isLocationChildrenDetail(detail)) {
return processChildrenDetail(document, target, detail);
} else {
// TODO: others...
const edge = document.computeRect(target);
return edge ? { edge, insertType: 'cover', coverRect: edge } : {};
}
}
@observer
export class InsertionView extends Component {
shouldComponentUpdate() {
return false;
}
render() {
const doc = getCurrentDocument();
if (!doc || !doc.dropLocation) {
return null;
}
const { scale, scrollTarget } = doc.viewport;
const sx = scrollTarget!.left;
const sy = scrollTarget!.top;
const { edge, insertType, coverRect, nearRect, vertical } = processDetail(doc.dropLocation);
if (!edge) {
return null;
}
let className = 'my-insertion';
const style: any = {};
let x: number;
let y: number;
if (insertType === 'cover') {
className += ' cover';
x = (coverRect!.left + sx) * scale;
y = (coverRect!.top + sy) * scale;
style.width = coverRect!.width * scale;
style.height = coverRect!.height * scale;
} else {
if (!nearRect) {
return null;
}
if (vertical) {
className += ' vertical';
x = ((insertType === 'before' ? nearRect.left : nearRect.right) + sx) * scale;
y = (nearRect.top + sy) * scale;
style.height = nearRect!.height * scale;
} else {
x = (nearRect.left + sx) * scale;
y = ((insertType === 'before' ? nearRect.top : nearRect.bottom) + sy) * scale;
style.width = nearRect.width * scale;
}
}
style.transform = `translate3d(${x}px, ${y}px, 0)`;
return <div className={className} style={style} />;
}
}

View File

@ -0,0 +1,60 @@
import { obx } from '@ali/recore';
import { INode } from '../../document/node';
export default class OffsetObserver {
@obx.ref offsetTop = 0;
@obx.ref offsetLeft = 0;
@obx.ref offsetRight = 0;
@obx.ref offsetBottom = 0;
@obx.ref height = 0;
@obx.ref width = 0;
@obx.ref hasOffset = false;
@obx.ref left = 0;
@obx.ref top = 0;
@obx.ref right = 0;
@obx.ref bottom = 0;
private pid: number | undefined;
constructor(node: INode) {
const document = node.document;
const scrollTarget = document.viewport.scrollTarget!;
let pid: number;
const compute = () => {
if (pid !== this.pid) {
return;
}
const rect = document.computeRect(node);
if (!rect) {
this.hasOffset = false;
return;
}
this.hasOffset = true;
this.offsetLeft = rect.left + scrollTarget.left;
this.offsetRight = rect.right + scrollTarget.left;
this.offsetTop = rect.top + scrollTarget.top;
this.offsetBottom = rect.bottom + scrollTarget.top;
this.height = rect.height;
this.width = rect.width;
this.left = rect.left;
this.top = rect.top;
this.right = rect.right;
this.bottom = rect.bottom;
this.pid = pid = (window as any).requestIdleCallback(compute);
};
// try first
compute();
// try second, ensure the dom mounted
this.pid = pid = (window as any).requestIdleCallback(compute);
}
destroy() {
if (this.pid) {
(window as any).cancelIdleCallback(this.pid);
}
this.pid = undefined;
}
}

View File

@ -0,0 +1,39 @@
.my-selecting {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
border: 1px solid var(--color-brand-light);
z-index: 2;
overflow: visible;
>.title {
position: absolute;
color: var(--color-brand-light);
top: -20px;
left: 0;
font-weight: lighter;
}
&.dragging {
background: rgba(182, 178, 178, 0.8);
border: none;
pointer-events: all;
}
&.x-shadow {
border-color: rgba(147, 112, 219, 1.0);
background: rgba(147, 112, 219, 0.04);
>.title {
color: rgba(147, 112, 219, 1.0);
}
&.highlight {
background: transparent;
}
}
&.x-flow {
border-color: rgb(255, 99, 8);
>.title {
color: rgb(255, 99, 8);
}
}
}

View File

@ -0,0 +1,85 @@
import { observer } from '@ali/recore';
import { Component, Fragment } from 'react';
import classNames from 'classnames';
import { INode, isElementNode, isConfettiNode, hasConditionFlow } from '../../document/node';
import OffsetObserver from './offset-observer';
import './selecting.less';
import { isShadowNode, isShadowsContainer } from '../../document/node/shadow-node';
import { isConditionFlow } from '../../document/node/condition-flow';
import { current, dragon } from '../../globals';
@observer
export class SingleSelectingView extends Component<{ node: INode; highlight?: boolean }> {
private offsetObserver: OffsetObserver;
constructor(props: { node: INode; highlight?: boolean }) {
super(props);
this.offsetObserver = new OffsetObserver(props.node);
}
render() {
if (!this.offsetObserver.hasOffset) {
return null;
}
const scale = this.props.node.document.viewport.scale;
const { width, height, offsetTop, offsetLeft } = this.offsetObserver;
const style = {
width: width * scale,
height: height * scale,
transform: `translate3d(${offsetLeft * scale}px, ${offsetTop * scale}px, 0)`,
} as any;
const { node, highlight } = this.props;
const className = classNames('my-selecting', {
'x-shadow': isShadowNode(node),
'x-flow': hasConditionFlow(node) || isConditionFlow(node),
highlight,
});
return <div className={className} style={style} />;
}
}
@observer
export class SelectingView extends Component {
get selecting(): INode[] {
const sel = current.selection;
if (!sel) {
return [];
}
if (dragon.dragging) {
return sel.getTopNodes();
}
return sel.getNodes();
}
render() {
return this.selecting.map(node => {
// select all nodes when doing x-for
if (isShadowsContainer(node)) {
// FIXME: thinkof nesting for
const views = [];
for (const shadowNode of (node as any).getShadows()!.values()) {
views.push(<SingleSelectingView key={shadowNode.id} node={shadowNode} />);
}
return <Fragment key={node.id}>{views}</Fragment>;
} else if (isShadowNode(node)) {
const shadows = node.origin.getShadows()!.values();
const views = [];
for (const shadowNode of shadows) {
views.push(<SingleSelectingView highlight={shadowNode === node} key={shadowNode.id} node={shadowNode} />);
}
return <Fragment key={node.id}>{views}</Fragment>;
}
// select the visible node when doing x-if
else if (isConditionFlow(node)) {
return <SingleSelectingView node={node.visibleNode} key={node.id} />;
}
return <SingleSelectingView node={node} key={node.id} />;
});
}
}

View File

@ -0,0 +1,80 @@
import { getCurrentAdaptor } from '../globals';
import Simulator from '../adaptors/simulator';
import { isCSSUrl } from '../utils/is-css-url';
export interface AssetMap {
jsUrl?: string;
cssUrl?: string;
jsText?: string;
cssText?: string;
}
export type AssetList = string[];
export type Assets = AssetMap[] | AssetList;
function isAssetMap(obj: any): obj is AssetMap {
return obj && typeof obj === 'object';
}
export function createSimulator<T, I>(iframe: HTMLIFrameElement, vendors: Assets = []): Promise<Simulator<T, I>> {
const currentAdaptor = getCurrentAdaptor();
const win: any = iframe.contentWindow;
const doc = iframe.contentDocument!;
const styles: string[] = [];
let scripts: string[] = [];
const afterScripts: string[] = [];
vendors.forEach((asset: any) => {
if (!isAssetMap(asset)) {
if (isCSSUrl(asset)) {
asset = { cssUrl: asset };
} else {
if (asset.startsWith('!')) {
afterScripts.push(`<script src="${asset.slice(1)}"></script>`);
return;
}
asset = { jsUrl: asset };
}
}
if (asset.jsText) {
scripts.push(`<script>${asset.jsText}</script>`);
}
if (asset.jsUrl) {
scripts.push(`<script src="${asset.jsUrl}"></script>`);
}
if (asset.cssUrl) {
styles.push(`<link rel="stylesheet" href="${asset.cssUrl}" />`);
}
if (asset.cssText) {
styles.push(`<style type="text/css">${asset.cssText}</style>`);
}
});
currentAdaptor.simulatorUrls.forEach(url => {
if (isCSSUrl(url)) {
styles.push(`<link rel="stylesheet" href="${url}" />`);
} else {
scripts.push(`<script src="${url}"></script>`);
}
});
scripts = scripts.concat(afterScripts);
doc.open();
doc.write(`<!doctype html><html><head><meta charset="utf-8"/>
${styles.join('\n')}
<style base-point></style>
${scripts.join('\n')}
</head></html>`);
doc.close();
return new Promise(resolve => {
if (win.VisionSimulator) {
return resolve(win.VisionSimulator);
}
const loaded = () => {
resolve(win.VisionSimulator);
win.removeEventListener('load', loaded);
};
win.addEventListener('load', loaded);
});
}

View File

@ -0,0 +1,47 @@
.my-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 10px;
box-shadow: 0 2px 10px 0 rgba(31,56,88,.15);
}
html.my-show-topbar .my-canvas {
top: var(--topbar-height);
}
html.my-show-toolbar .my-canvas {
top: var(--toolbar-height);
}
html.my-show-topbar.my-show-toolbar .my-canvas {
top: calc(var(--topbar-height) + var(--topbar-height));
}
.my-screen {
top: 0;
bottom: 0;
width: 100%;
left: 0;
position: absolute;
overflow: hidden;
}
.my-doc-shell {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
.my-doc-frame {
border: none;
transform-origin: 0 0;
height: 100%;
width: 100%;
}
}
.my-drag-pane-mode .my-doc-shell {
pointer-events: none;
}

View File

@ -0,0 +1,76 @@
import { Component } from 'react';
import { observer } from '@ali/recore';
import { getCurrentDocument, screen, progressing } from '../../globals';
import { AutoFit } from '../../document/viewport';
import { AuxiliaryView } from '../auxiliary';
import { PreLoaderView } from '../widgets/pre-loader';
import DocumentContext from '../../document/document-context';
import FocusingArea from '../widgets/focusing-area';
import './canvas.less';
const Canvas = () => (
<FocusingArea
className="my-canvas"
id="canvas"
onEsc={() => {
const doc = getCurrentDocument();
if (doc) {
doc.selection.clear();
}
return false;
}}
>
<Screen />
</FocusingArea>
);
export default Canvas;
@observer
class Screen extends Component {
render() {
const doc = getCurrentDocument();
// TODO: thinkof multi documents
return (
<div ref={elmt => screen.mount(elmt)} className="my-screen">
{progressing.visible ? <PreLoaderView /> : null}
<AuxiliaryView />
{doc ? <DocumentView key={doc.id} doc={doc} /> : null}
</div>
);
}
}
@observer
class DocumentView extends Component<{ doc: DocumentContext }> {
componentWillUnmount() {
this.props.doc.sleep();
}
render() {
const { doc } = this.props;
const viewport = doc.viewport;
let shellStyle = {};
let frameStyle = {};
if (viewport.width !== AutoFit && viewport.height !== AutoFit) {
const shellWidth = viewport.width * viewport.scale;
const screenWidth = screen.width;
const shellLeft = shellWidth < screenWidth ? `calc((100% - ${shellWidth}px) / 2)` : 0;
shellStyle = {
width: shellWidth,
left: shellLeft,
};
frameStyle = {
transform: `scale(${viewport.scale})`,
height: viewport.height,
width: viewport.width,
};
}
return (
<div className="my-doc-shell" style={shellStyle}>
<iframe className="my-doc-frame" style={frameStyle} ref={frame => doc.mountRuntimeFrame(frame)} />
</div>
);
}
}

View File

@ -0,0 +1,18 @@
class Designer {
id: string = guid();
hotkey: Hotkey;
constructor(options: BuilderOptions): Builder;
getValue(): ProjectSchema;
setValue(schema: ProjectSchema): void;
project: Project;
dragboost(locateEvent: LocateEvent): void;
addDropSensor(dropSensor: DropSensor): void;
// 事件 & 消息
onActiveChange(): () => void;
onDragstart(): void;
onDragend(): void;
//....
}

View File

@ -0,0 +1,173 @@
import Project from '../project';
import { RootSchema, NodeData, isDOMText, isJSExpression } from '../schema';
export default class DocumentContext {
/**
*
*/
readonly id: string;
/**
*
*/
readonly selection: Selection = new Selection(this);
/**
*
*/
readonly history: History = new History(this);
/**
* Page/Component/Block
*/
readonly root: Root;
/**
*
*/
simulator?: SimulatorInterface;
private nodesMap = new Map<string, INode>();
private nodes = new Set<INode>();
private seqId = 0;
constructor(readonly project: Project, schema: RootSchema) {
this.id = uniqueId('doc');
this.root = new Root(this, viewData);
}
/**
* id
*/
getNode(id: string): Node | null {
return this.nodesMap.get(id) || null;
}
/**
* schema
*/
createNode(data: NodeData): Node {
let schema: any;
if (isDOMText(data)) {
schema = {
componentName: '#text',
children: data,
};
} else if (isJSExpression(data)) {
schema = {
componentName: '#expression',
children: data,
};
} else {
schema = data;
}
const node = new Node(this, schema);
this.nodesMap.set(node.id, node);
this.nodes.add(node);
return node;
}
/**
*
*/
insertNode(parent: Node, thing: Node | Schema, at?: number | null, copy?: boolean): Node;
/**
*
*/
removeNode(idOrNode: string | Node) {
let id: string;
let node: Node | null;
if (typeof idOrNode === 'string') {
id = idOrNode;
node = this.getNode(id);
} else {
node = idOrNode;
id = node.id;
}
if (!node || !node.parent) {
return;
}
this.nodesMap.delete(id);
this.nodes.delete(node);
node.parent.removeChild(node);
}
/**
* schema
*/
getSchema(): Schema {
return this.root.getSchema();
}
/**
* Schema
*/
getNodeSchema(id: string): Schema | null {
const node = this.getNode(id);
if (node) {
return node.getSchema();
}
return null;
}
/**
* simulator
*/
getViewInstance(node: Node): ViewInstance[] | null {
if (this.simulator) {
this.simulator.getViewInstance(node.id);
}
return null;
}
/**
* DOM simulator
*/
getNodeFromElement(target: Element | null): Node | null {
if (!this.simulator || !target) {
return null;
}
const id = this.simulator.getClosestNodeId(target);
if (!id) {
return null;
}
return this.getNode(id) as Node;
}
/**
*
* DOM simulator
*/
getDOMNodes(viewInstance: ViewInstance): Array<Element | Text> | null {
if (!this.simulator) {
return null;
}
if (isElement(viewInstance)) {
return [viewInstance];
}
return this.simulator.findDOMNodes(viewInstance);
}
/**
*
*/
active(): void {}
/**
*
*/
suspense(): void {}
/**
*
*/
destroy(): void {}
/**
*
*/
isModified() {
return !this.history.isSavePoint();
}
/**
* id
*/
nextId() {
return (++this.seqId).toString(36).toLocaleLowerCase();
}
getComponent(tagName: string): any {
return this.simulator!.getCurrentComponent(tagName);
}
}

View File

@ -0,0 +1,123 @@
import { INode, INodeParent } from './node';
import DocumentContext from './document-context';
export interface LocationData {
target: INodeParent; // shadowNode | ConditionFlow | ElementNode | RootNode
detail: LocationDetail;
}
export enum LocationDetailType {
Children = 'Children',
Prop = 'Prop',
}
export interface LocationChildrenDetail {
type: LocationDetailType.Children;
index: number;
near?: {
node: INode;
pos: 'before' | 'after';
rect?: Rect;
align?: 'V' | 'H';
};
}
export interface LocationPropDetail {
// cover 形态,高亮 domNode如果 domNode 为空,取 container 的值
type: LocationDetailType.Prop;
name: string;
domNode?: HTMLElement;
}
export type LocationDetail = LocationChildrenDetail | LocationPropDetail | { type: string; [key: string]: any };
export interface Point {
clientX: number;
clientY: number;
}
export type Rects = Array<ClientRect | DOMRect> & {
elements: Array<Element | Text>;
};
export type Rect = (ClientRect | DOMRect) & {
elements: Array<Element | Text>;
computed?: boolean;
};
export function isLocationData(obj: any): obj is LocationData {
return obj && obj.target && obj.detail;
}
export function isLocationChildrenDetail(obj: any): obj is LocationChildrenDetail {
return obj && obj.type === LocationDetailType.Children;
}
export function isRowContainer(container: Element | Text, win?: Window) {
if (isText(container)) {
return true;
}
const style = (win || getWindow(container)).getComputedStyle(container);
const display = style.getPropertyValue('display');
if (/flex$/.test(display)) {
const direction = style.getPropertyValue('flex-direction') || 'row';
if (direction === 'row' || direction === 'row-reverse') {
return true;
}
}
return false;
}
export function isChildInline(child: Element | Text, win?: Window) {
if (isText(child)) {
return true;
}
const style = (win || getWindow(child)).getComputedStyle(child);
return /^inline/.test(style.getPropertyValue('display'));
}
export function getRectTarget(rect: Rect | null) {
if (!rect || rect.computed) {
return null;
}
const els = rect.elements;
return els && els.length > 0 ? els[0]! : null;
}
export function isVerticalContainer(rect: Rect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isRowContainer(el);
}
export function isVertical(rect: Rect | null) {
const el = getRectTarget(rect);
if (!el) {
return false;
}
return isChildInline(el) || (el.parentElement ? isRowContainer(el.parentElement) : false);
}
function isText(elem: any): elem is Text {
return elem.nodeType === Node.TEXT_NODE;
}
function isDocument(elem: any): elem is Document {
return elem.nodeType === Node.DOCUMENT_NODE;
}
export function getWindow(elem: Element | Document): Window {
return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!;
}
export default class Location {
readonly target: INodeParent;
readonly detail: LocationDetail;
constructor(readonly document: DocumentContext, { target, detail }: LocationData) {
this.target = target;
this.detail = detail;
}
}

View File

@ -0,0 +1,733 @@
import RootNode from './root-node';
import { flags, panes } from '../globals';
import {
dragon,
ISenseAble,
isShaken,
LocateEvent,
isNodesDragTarget,
NodesDragTarget,
NodeDatasDragTarget,
isNodeDatasDragTarget,
DragTargetType,
isAnyDragTarget,
} from '../globals/dragon';
import cursor from '../utils/cursor';
import {
INode,
isElementNode,
isNode,
INodeParent,
insertChildren,
hasConditionFlow,
contains,
isRootNode,
isConfettiNode,
} from './node';
import {
Point,
Rect,
getRectTarget,
isChildInline,
isRowContainer,
LocationDetailType,
LocationChildrenDetail,
isLocationChildrenDetail,
LocationData,
isLocationData,
} from './location';
import { isConditionFlow } from './node/condition-flow';
import { isElementData, NodeData } from './document-data';
import ElementNode from './node/element-node';
import { AT_CHILD } from '../prototype/prototype';
import Scroller from './scroller';
import { isShadowNode } from './node/shadow-node';
import { activeTracker } from '../globals/active-tracker';
import { edging } from '../globals/edging';
import { setNativeSelection } from '../utils/navtive-selection';
import DocumentContext from './document-context';
import Simulator from '../adaptors/simulator';
import { focusing } from '../globals/focusing';
import embedEditor from '../globals/embed-editor';
export const MasterBoardID = 'master-board';
export default class MasterBoard implements ISenseAble {
id = MasterBoardID;
sensitive = true;
readonly contentDocument: Document;
private simulator: Simulator<any, any>;
private sensing = false;
private scroller: Scroller;
get bounds() {
const vw = this.document.viewport;
const bounds = vw.bounds;
const innerBounds = vw.innerBounds;
const doe = this.contentDocument.documentElement;
return {
top: bounds.top,
left: bounds.left,
right: bounds.right,
bottom: bounds.bottom,
width: bounds.width,
height: bounds.height,
innerBounds,
scale: vw.scale,
scrollHeight: doe.scrollHeight,
scrollWidth: doe.scrollWidth,
};
}
constructor(readonly document: DocumentContext, frame: HTMLIFrameElement) {
this.simulator = document.simulator!;
this.contentDocument = frame.contentDocument!;
this.scroller = new Scroller(this, document.viewport.scrollTarget!);
const doc = this.contentDocument;
const selection = document.selection;
// TODO: think of lock when edit a node
// 事件路由
doc.addEventListener('mousedown', (downEvent: MouseEvent) => {
/*
if (embedEditor.editing) {
return;
}
*/
const target = document.getNodeFromElement(downEvent.target as Element);
panes.dockingStation.visible = false;
focusing.focus('canvas');
if (!target) {
selection.clear();
return;
}
const isMulti = downEvent.metaKey || downEvent.ctrlKey;
const isLeftButton = downEvent.which === 1 || downEvent.button === 0;
if (isLeftButton) {
let node: INode = target;
if (hasConditionFlow(node)) {
node = node.conditionFlow;
}
let nodes: INode[] = [node];
let ignoreUpSelected = false;
if (isMulti) {
// multi select mode, directily add
if (!selection.has(node.id)) {
activeTracker.track(node);
selection.add(node.id);
ignoreUpSelected = true;
}
// 获得顶层 nodes
nodes = selection.getTopNodes();
} else if (selection.containsNode(target)) {
nodes = selection.getTopNodes();
} else {
// will clear current selection & select dragment in dragstart
}
dragon.boost(
{
type: DragTargetType.Nodes,
nodes,
},
downEvent,
);
if (ignoreUpSelected) {
// multi select mode has add selected, should return
return;
}
}
const checkSelect = (e: MouseEvent) => {
doc.removeEventListener('mouseup', checkSelect, true);
if (!isShaken(downEvent, e)) {
// const node = hasConditionFlow(target) ? target.conditionFlow : target;
const node = target;
const id = node.id;
activeTracker.track(node);
if (isMulti && selection.has(id)) {
selection.del(id);
} else {
selection.select(id);
}
}
};
doc.addEventListener('mouseup', checkSelect, true);
});
dragon.onDragstart(({ dragTarget }) => {
if (this.disableEdging) {
this.disableEdging();
}
if (isNodesDragTarget(dragTarget) && dragTarget.nodes.length === 1) {
// ensure current selecting
selection.select(dragTarget.nodes[0].id);
}
flags.setDragComponentsMode(true);
});
dragon.onDragend(({ dragTarget, copy }) => {
const loc = this.document.dropLocation;
flags.setDragComponentsMode(false);
if (loc) {
if (!isConditionFlow(loc.target)) {
if (isLocationChildrenDetail(loc.detail)) {
let nodes: INode[] | undefined;
if (isNodesDragTarget(dragTarget)) {
nodes = insertChildren(loc.target, dragTarget.nodes, loc.detail.index, copy);
} else if (isNodeDatasDragTarget(dragTarget)) {
// process nodeData
const nodesData = this.document.processDocumentData(dragTarget.data, dragTarget.maps);
nodes = insertChildren(loc.target, nodesData, loc.detail.index);
}
if (nodes) {
this.document.selection.selectAll(nodes.map(o => o.id));
setTimeout(() => activeTracker.track(nodes![0]), 10);
}
}
// TODO: others...
}
}
this.document.clearLocation();
this.enableEdging();
});
// cause edit
doc.addEventListener('dblclick', (e: MouseEvent) => {
// TODO: refactor
let target = document.getNodeFromElement(e.target as Element)!;
if (target && isElementNode(target)) {
if (isShadowNode(target)) {
target = target.origin;
}
if (target.children.length === 1 && isConfettiNode(target.children[0])) {
// test
// embedEditor.edit(target as any, 'children', document.getDOMNodes(target) as any);
activeTracker.track(target.children[0]);
selection.select(target.children[0].id);
}
}
});
activeTracker.onChange(({ node, detail }) => {
this.scrollToNode(node, detail);
});
this.enableEdging();
}
private disableEdging: (() => void) | undefined;
enableEdging() {
const edgingWatch = (e: Event) => {
const node = this.document.getNodeFromElement(e.target as Element);
edging.watch(node);
e.stopPropagation();
};
const leave = () => edging.watch(null);
this.contentDocument.addEventListener('mouseover', edgingWatch, true);
this.contentDocument.addEventListener('mouseleave', leave, false);
// TODO: refactor this line, contains click, mousedown, mousemove
this.contentDocument.addEventListener(
'mousemove',
(e: Event) => {
e.stopPropagation();
},
true,
);
this.disableEdging = () => {
edging.watch(null);
this.contentDocument.removeEventListener('mouseover', edgingWatch, true);
this.contentDocument.removeEventListener('mouseleave', leave, false);
};
}
setNativeSelection(enableFlag: boolean) {
setNativeSelection(enableFlag);
this.simulator.utils.setNativeSelection(enableFlag);
}
setDragging(flag: boolean) {
cursor.setDragging(flag);
this.simulator.utils.cursor.setDragging(flag);
}
setCopy(flag: boolean) {
cursor.setCopy(flag);
this.simulator.utils.cursor.setCopy(flag);
}
isCopy(): boolean {
return this.simulator.utils.cursor.isCopy();
}
releaseCursor() {
cursor.release();
this.simulator.utils.cursor.release();
}
getDropTarget(e: LocateEvent): INodeParent | LocationData | null {
const { target, dragTarget } = e;
const isAny = isAnyDragTarget(dragTarget);
let container: any;
if (target) {
const ref = this.document.getNodeFromElement(target as Element);
if (ref) {
container = ref;
} else if (isAny) {
return null;
} else {
container = this.document.view;
}
} else if (isAny) {
return null;
} else {
container = this.document.view;
}
if (!isElementNode(container) && !isRootNode(container)) {
container = container.parent;
}
// use spec container to accept specialData
if (isAny) {
while (container) {
if (isRootNode(container)) {
return null;
}
const locationData = this.acceptAnyData(container, e);
if (locationData) {
return locationData;
}
container = container.parent;
}
return null;
}
let res: any;
let upward: any;
// TODO: improve AT_CHILD logic, mark has checked
while (container) {
res = this.acceptNodes(container, e);
if (isLocationData(res)) {
return res;
}
if (res === true) {
return container;
}
if (!res) {
if (upward) {
container = upward;
upward = null;
} else {
container = container.parent;
}
} else if (res === AT_CHILD) {
if (!upward) {
upward = container.parent;
}
container = this.getNearByContainer(container, e);
if (!container) {
container = upward;
upward = null;
}
} else if (isNode(res)) {
container = res;
upward = null;
}
}
return null;
}
acceptNodes(container: RootNode | ElementNode, e: LocateEvent) {
const { dragTarget } = e;
if (isRootNode(container)) {
return this.checkDropTarget(container, dragTarget as any);
}
const proto = container.prototype;
const acceptable: boolean = this.isAcceptable(container);
if (!proto.isContainer && !acceptable) {
return false;
}
// check is contains, get common parent
if (isNodesDragTarget(dragTarget)) {
const nodes = dragTarget.nodes;
let i = nodes.length;
let p: any = container;
while (i-- > 0) {
if (contains(nodes[i], p)) {
p = nodes[i].parent;
}
}
if (p !== container) {
return p || this.document.view;
}
}
// first use accept
if (acceptable) {
const view: any = this.document.getView(container);
if (view && '$accept' in view) {
if (view.$accept === false) {
return false;
}
if (view.$accept === AT_CHILD || view.$accept === '@CHILD') {
return AT_CHILD;
}
if (typeof view.$accept === 'function') {
const ret = view.$accept(container, e);
if (ret || ret === false) {
return ret;
}
}
}
if (proto.acceptable) {
const ret = proto.accept(container, e);
if (ret || ret === false) {
return ret;
}
}
}
return this.checkNesting(container, dragTarget as any);
}
getNearByContainer(container: INodeParent, e: LocateEvent): INodeParent | null {
const children = container.children;
if (!children || children.length < 1) {
return null;
}
let nearDistance: any = null;
let nearBy: any = null;
for (let i = 0, l = children.length; i < l; i++) {
let child: any = children[i];
if (!isElementNode(child)) {
continue;
}
if (hasConditionFlow(child)) {
const bn = child.conditionFlow;
i = bn.index + bn.length - 1;
child = bn.visibleNode;
}
const rect = this.document.computeRect(child);
if (!rect) {
continue;
}
if (isPointInRect(e, rect)) {
return child;
}
const distance = distanceToRect(e, rect);
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearBy = child;
}
}
return nearBy;
}
locate(e: LocateEvent): any {
this.sensing = true;
this.scroller.scrolling(e);
const dropTarget = this.getDropTarget(e);
if (!dropTarget) {
return null;
}
if (isLocationData(dropTarget)) {
return this.document.createLocation(dropTarget);
}
const target = dropTarget;
const edge = this.document.computeRect(target);
const children = target.children;
const detail: LocationChildrenDetail = {
type: LocationDetailType.Children,
index: 0,
};
const locationData = {
target,
detail,
};
if (!children || children.length < 1 || !edge) {
return this.document.createLocation(locationData);
}
let nearRect = null;
let nearIndex = 0;
let nearNode = null;
let nearDistance = null;
let top = null;
let bottom = null;
for (let i = 0, l = children.length; i < l; i++) {
let node = children[i];
let index = i;
if (hasConditionFlow(node)) {
node = node.conditionFlow;
index = node.index;
// skip flow items
i = index + (node as any).length - 1;
}
const rect = this.document.computeRect(node);
if (!rect) {
continue;
}
const distance = isPointInRect(e, rect) ? 0 : distanceToRect(e, rect);
if (distance === 0) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
break;
}
// TODO: 忘记为什么这么处理了,记得添加注释
if (top === null || rect.top < top) {
top = rect.top;
}
if (bottom === null || rect.bottom > bottom) {
bottom = rect.bottom;
}
if (nearDistance === null || distance < nearDistance) {
nearDistance = distance;
nearNode = node;
nearIndex = index;
nearRect = rect;
}
}
detail.index = nearIndex;
if (nearNode && nearRect) {
const el = getRectTarget(nearRect);
const inline = el ? isChildInline(el) : false;
const row = el ? isRowContainer(el.parentElement!) : false;
const vertical = inline || row;
// TODO: fix type
const near: any = {
node: nearNode,
pos: 'before',
align: vertical ? 'V' : 'H',
};
detail.near = near;
if (isNearAfter(e, nearRect, vertical)) {
near.pos = 'after';
detail.index = nearIndex + (isConditionFlow(nearNode) ? nearNode.length : 1);
}
if (!row && nearDistance !== 0) {
const edgeDistance = distanceToEdge(e, edge);
if (edgeDistance.distance < nearDistance!) {
const nearAfter = edgeDistance.nearAfter;
if (top == null) {
top = edge.top;
}
if (bottom == null) {
bottom = edge.bottom;
}
near.rect = new DOMRect(edge.left, top, edge.width, bottom - top);
near.align = 'H';
near.pos = nearAfter ? 'after' : 'before';
detail.index = nearAfter ? children.length : 0;
}
}
}
return this.document.createLocation(locationData);
}
private tryScrollAgain: number | null = null;
scrollToNode(node: INode, detail?: any, tryTimes = 0) {
this.tryScrollAgain = null;
if (this.sensing) {
// actived sensor
return;
}
const opt: any = {};
let scroll = false;
if (detail) {
// TODO:
/*
const rect = insertion ? insertion.getNearRect() : node.getRect();
let y;
let scroll = false;
if (insertion && rect) {
y = insertion.isNearAfter() ? rect.bottom : rect.top;
if (y < bounds.top || y > bounds.bottom) {
scroll = true;
}
}*/
} else {
const rect = this.document.computeRect(node);
if (!rect || rect.width === 0 || rect.height === 0) {
if (!this.tryScrollAgain && tryTimes < 3) {
this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(node, null, tryTimes + 1));
}
return;
}
const scrollTarget = this.document.viewport.scrollTarget!;
const st = scrollTarget.top;
const sl = scrollTarget.left;
const { innerBounds, scrollHeight, scrollWidth } = this.bounds;
const { height, width, top, bottom, left, right } = innerBounds;
if (rect.height > height ? rect.top > bottom || rect.bottom < top : rect.top < top || rect.bottom > bottom) {
opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height);
scroll = true;
}
if (rect.width > width ? rect.left > right || rect.right < left : rect.left < left || rect.right > right) {
opt.left = Math.min(rect.left + rect.width / 2 + sl - left - width / 2, scrollWidth - width);
scroll = true;
}
}
if (scroll && this.scroller) {
this.scroller.scrollTo(opt);
}
}
fixEvent(e: LocateEvent): LocateEvent {
if (e.fixed) {
return e;
}
if (!e.target || e.originalEvent.view!.document !== this.contentDocument) {
e.target = this.contentDocument.elementFromPoint(e.clientX, e.clientY);
}
return e;
}
isEnter(e: LocateEvent): boolean {
const rect = this.bounds;
return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right;
}
inRange(e: LocateEvent): boolean {
return e.globalX <= this.bounds.right;
}
deactive() {
this.sensing = false;
this.scroller.cancel();
}
isAcceptable(container: ElementNode): boolean {
const proto = container.prototype;
const view: any = this.document.getView(container);
if (view && '$accept' in view) {
return true;
}
return proto.acceptable;
}
acceptAnyData(container: ElementNode, e: LocateEvent | MouseEvent | KeyboardEvent) {
const proto = container.prototype;
const view: any = this.document.getView(container);
// use view instance method: $accept
if (view && typeof view.$accept === 'function') {
// should return LocationData
return view.$accept(container, e);
}
// use prototype method: accept
return proto.accept(container, e);
}
checkNesting(dropTarget: ElementNode, dragTarget: NodesDragTarget | NodeDatasDragTarget): boolean {
const items: Array<INode | NodeData> = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data;
return items.every(item => this.checkNestingDown(dropTarget, item));
}
checkDropTarget(dropTarget: RootNode | ElementNode, dragTarget: NodesDragTarget | NodeDatasDragTarget): boolean {
const items: Array<INode | NodeData> = dragTarget.nodes || (dragTarget as NodeDatasDragTarget).data;
return items.every(item => this.checkNestingUp(dropTarget, item));
}
checkNestingUp(parent: RootNode | ElementNode, target: NodeData | INode): boolean {
if (isElementNode(target) || isElementData(target)) {
const proto = isElementNode(target)
? target.prototype
: this.document.getPrototypeByTagNameOrURI(target.tagName, target.uri);
if (proto) {
return proto.checkNestingUp(target, parent);
}
}
return true;
}
checkNestingDown(parent: ElementNode, target: NodeData | INode): boolean {
const proto = parent.prototype;
if (isConditionFlow(parent)) {
return parent.children.every(
child => proto.checkNestingDown(parent, child) && this.checkNestingUp(parent, child),
);
} else {
return proto.checkNestingDown(parent, target) && this.checkNestingUp(parent, target);
}
}
}
function isPointInRect(point: Point, rect: Rect) {
return (
point.clientY >= rect.top &&
point.clientY <= rect.bottom &&
point.clientX >= rect.left &&
point.clientX <= rect.right
);
}
function distanceToRect(point: Point, rect: Rect) {
let minX = Math.min(Math.abs(point.clientX - rect.left), Math.abs(point.clientX - rect.right));
let minY = Math.min(Math.abs(point.clientY - rect.top), Math.abs(point.clientY - rect.bottom));
if (point.clientX >= rect.left && point.clientX <= rect.right) {
minX = 0;
}
if (point.clientY >= rect.top && point.clientY <= rect.bottom) {
minY = 0;
}
return Math.sqrt(minX ** 2 + minY ** 2);
}
function distanceToEdge(point: Point, rect: Rect) {
const distanceTop = Math.abs(point.clientY - rect.top);
const distanceBottom = Math.abs(point.clientY - rect.bottom);
return {
distance: Math.min(distanceTop, distanceBottom),
nearAfter: distanceBottom < distanceTop,
};
}
function isNearAfter(point: Point, rect: Rect, inline: boolean) {
if (inline) {
return (
Math.abs(point.clientX - rect.left) + Math.abs(point.clientY - rect.top) >
Math.abs(point.clientX - rect.right) + Math.abs(point.clientY - rect.bottom)
);
}
return Math.abs(point.clientY - rect.top) > Math.abs(point.clientY - rect.bottom);
}

View File

@ -0,0 +1,478 @@
import { NodeSchema, isNodeSchema, NodeData, DOMText, JSExpression, PropsMap } from '../schema';
import Props, { Prop } from './props';
/**
* nodeSchema are:
* [basic]
* .componentName
* .props
* .children
* [directive]
* .condition
* .loop
* .loopArgs
* [addon]
* .conditionGroup = 'abc' // 当移动时值会改
* .title
* .ignore
* .hidden
* .locked
*/
export default class Node {
/**
*
*/
readonly isNode = true;
/**
* id
*/
readonly id: string;
/**
*
* :
* * #text
* * #expression
* * Page
* * Block/Fragment
* * Component /
*/
readonly componentName: string;
constructor(readonly document: DocumentContext, private nodeSchema: NodeSchema) {
const { componentName, id, children, props, leadingComponents, ...directives } = nodeSchema;
// clone
this.id = id || `node$${document.nextId()}`;
this.componentName = componentName;
if (this.isNodeParent()) {
this._props = new Props(this, props);
this._directives = new Props(this, directives as PropsMap);
if (children) {
this._children = (Array.isArray(children) ? children : [children]).map(child => {
const node = this.document.createNode(child);
node.internalSetParent(this);
return node;
});
}
}
}
/**
*
*/
isNodeParent(): boolean {
return this.componentName.charAt(0) !== '#';
}
private _parent: Node | null = null;
/**
*
*/
get parent(): Node | null {
return this._parent;
}
/**
*
*
* @ignore
*/
internalSetParent(parent: Node | null) {
if (this._parent === parent) {
return;
}
if (this._parent) {
removeChild(this._parent, this);
}
this._parent = parent;
if (parent) {
this._zLevel = parent.zLevel + 1;
} else {
this._zLevel = -1;
}
}
private _zLevel = 0;
/**
*
*/
get zLevel(): number {
return this._zLevel;
}
private _children: Node[] | null = null;
/**
*
*/
get children(): Node[] | null {
if (this.purged) {
return [];
}
if (this._children) {
return this._children;
}
}
@obx.ref get component(): ReactType {
return this.document.getComponent(this.tagName);
}
@obx.ref get prototype(): Prototype {
return this.document.getPrototype(this.component, this.tagName);
}
@obx.ref get props(): object {
if (!this.isNodeParent() || this.componentName === 'Fragment') {
return {};
}
// ...
}
private _directives: any = {};
get directives() {
return {
condition: this.condition,
conditionGroup: this.conditionGroup,
loop: '',
};
}
private _conditionGroup: string | null = null;
/**
*
*/
get conditionGroup(): string | null {
if (this._conditionGroup) {
return this._conditionGroup;
}
// 如果 condition 有值,且没有 group
if (this._condition) {
return this.id;
}
return null;
}
set conditionGroup(val) {
this._conditionGroup = val;
}
private _condition: any;
/**
*
*/
get condition() {
if (this._condition == null) {
if (this._conditionGroup) {
// FIXME: should be expression
return true;
}
return null;
}
return this._condition;
}
// 外部修改merge 进来,产生一次可恢复的历史数据
merge(data: ElementData) {
this.elementData = data;
const { leadingComments } = data;
this.leadingComments = leadingComments ? leadingComments.slice() : [];
this.parse();
this.mergeChildren(data.children || []);
}
private mergeChildren(data: NodeData[]) {
for (let i = 0, l = data.length; i < l; i++) {
const item = this.children[i];
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
item.merge(data[i]);
} else {
if (item) {
item.purge();
}
this.children[i] = this.document.createNode(data[i]);
this.children[i].internalSetParent(this);
}
}
if (this.children.length > data.length) {
this.children.splice(data.length).forEach(child => child.purge());
}
}
getProp(path: string): Prop;
getProp(path: string, useStash: true): Prop;
getProp(path: string, useStash = true): Prop | null {
return this._props!.query(path, useStash)!;
}
getProps(): Props {
return this._props;
}
/**
*
*/
get index(): number {
if (!this.parent) {
return -1;
}
return indexOf(this.parent, this);
}
/**
*
*/
get nextSibling(): Node | null {
if (!this.parent) {
return null;
}
const index = this.index;
if (index < 0) {
return null;
}
return getChildAt(this.parent, index + 1);
}
/**
*
*/
get prevSibling(): Node | null {
if (!this.parent) {
return null;
}
const index = this.index;
if (index < 1) {
return null;
}
return getChildAt(this.parent, index - 1);
}
/**
* - schema
*/
get schema(): NodeSchema {
return this.exportSchema();
}
/**
* schema
* @param serialize id
*/
exportSchema(serialize = false): NodeSchema {
const schema: any = {
componentName: this.componentName,
props: this.props,
condition: this.condition,
conditionGroup: this.conditionGroup,
...this.directives,
};
if (serialize) {
schema.id = this.id;
}
const children = this.children;
if (children && children.length > 0) {
schema.children = children.map(node => node.exportSchema(serialize));
}
return schema;
}
// TODO: 再利用历史数据,不产生历史数据
reuse(timelineData: NodeSchema) {}
/**
*
*/
contains(node: Node): boolean {
return contains(this, node);
}
/**
*
*/
getZLevelTop(zLevel: number): Node | null {
return getZLevelTop(this, zLevel);
}
/**
*
*
* 16 thisNode contains otherNode
* 8 thisNode contained_by otherNode
* 2 thisNode before or after otherNode
* 0 thisNode same as otherNode
*/
comparePosition(otherNode: Node): number {
return comparePosition(this, otherNode);
}
private purged = false;
/**
*
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.children.forEach(child => child.purge());
// TODO: others dispose...
}
}
export interface INodeParent extends Node {
readonly children: Node[];
}
export function isNode(node: any): node is Node {
return node && node.isNode;
}
export function getZLevelTop(child: Node, zLevel: number): Node | null {
let l = child.zLevel;
if (l < zLevel || zLevel < 0) {
return null;
}
if (l === zLevel) {
return child;
}
let r: any = child;
while (r && l-- > zLevel) {
r = r.parent;
}
return r;
}
export function contains(node1: Node, node2: Node): boolean {
if (node1 === node2) {
return true;
}
if (!node1.isNodeParent() || !node1.children || !node2.parent) {
return false;
}
const p = getZLevelTop(node2, node1.zLevel);
if (!p) {
return false;
}
return node1 === p;
}
// 16 node1 contains node2
// 8 node1 contained_by node2
// 2 node1 before or after node2
// 0 node1 same as node2
export function comparePosition(node1: Node, node2: Node): number {
if (node1 === node2) {
return 0;
}
const l1 = node1.zLevel;
const l2 = node2.zLevel;
if (l1 === l2) {
return 2;
}
let p: any;
if (l1 > l2) {
p = getZLevelTop(node2, l1);
if (p && p === node1) {
return 16;
}
return 2;
}
p = getZLevelTop(node1, l2);
if (p && p === node2) {
return 8;
}
return 2;
}
export function insertChild(container: INodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
let node: Node;
if (copy && isNode(thing)) {
thing = thing.schema;
}
if (isNode(thing)) {
node = thing;
} else {
node = container.document.createNode(thing);
}
const children = container.children;
let index = at == null ? children.length : at;
const i = children.indexOf(node);
if (i < 0) {
if (index < children.length) {
children.splice(index, 0, node);
} else {
children.push(node);
}
node.internalSetParent(container);
} else {
if (index > i) {
index -= 1;
}
if (index === i) {
return node;
}
children.splice(i, 1);
children.splice(index, 0, node);
}
// check condition group
node.conditionGroup = null;
if (node.prevSibling && node.nextSibling) {
const conditionGroup = node.prevSibling.conditionGroup;
if (conditionGroup && conditionGroup === node.nextSibling.conditionGroup) {
node.conditionGroup = conditionGroup;
}
}
return node;
}
export function insertChildren(
container: INodeParent,
nodes: Node[] | NodeSchema[],
at?: number | null,
copy?: boolean,
): Node[] {
let index = at;
let node: any;
const results: Node[] = [];
// tslint:disable-next-line
while ((node = nodes.pop())) {
results.push(insertChild(container, node, index, copy));
index = node.index;
}
return results;
}
export function getChildAt(parent: INodeParent, index: number): Node | null {
if (!parent.children) {
return null;
}
return parent.children[index];
}
export function indexOf(parent: INodeParent, child: Node): number {
if (!parent.children) {
return -1;
}
return parent.children.indexOf(child);
}
export function removeChild(parent: INodeParent, child: Node) {
if (!parent.children) {
return;
}
const i = parent.children.indexOf(child);
if (i > -1) {
parent.children.splice(i, 1);
}
}

View File

@ -0,0 +1,558 @@
import { untracked, computed, obx } from '@recore/obx';
import { uniqueId, isPlainObject, hasOwnProperty } from '../../utils';
import { valueToSource } from '../../utils/value-to-source';
import { CompositeValue, isJSExpression, PropsList, PropsMap } from '../schema';
import StashSpace from './stash-space';
export const UNSET = Symbol.for('unset');
export type UNSET = typeof UNSET;
export interface IPropParent {
delete(prop: Prop): void;
}
export class Prop implements IPropParent {
readonly isProp = true;
readonly id = uniqueId('prop$');
private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' = 'unset';
/**
*
*/
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' {
return this._type;
}
@obx.ref private _value: any = UNSET;
/**
*
*/
@computed get value(): CompositeValue {
if (this._type === 'unset') {
return null;
}
const type = this._type;
if (type === 'literal' || type === 'expression') {
return this._value;
}
if (type === 'map') {
if (!this._items) {
return this._value;
}
const maps: any = {};
this.items!.forEach((prop, key) => {
maps[key] = prop.value;
});
return maps;
}
if (type === 'list') {
if (!this._items) {
return this._items;
}
return this.items!.map(prop => prop.value);
}
return null;
}
/**
* set value, val should be JSON Object
*/
set value(val: CompositeValue) {
this._value = val;
const t = typeof val;
if (val == null) {
this._value = null;
this._type = 'literal';
} else if (t === 'string' || t === 'number' || t === 'boolean') {
this._value = val;
this._type = 'literal';
} else if (Array.isArray(val)) {
this._type = 'list';
} else if (isPlainObject(val)) {
if (isJSExpression(val)) {
this._type = 'expression';
} else {
this._type = 'map';
}
this._type = 'map';
} else {
this._type = 'expression';
this._value = {
type: 'JSExpression',
value: valueToSource(val),
};
}
if (untracked(() => this._items)) {
this._items!.forEach(prop => prop.purge());
this._items = null;
}
this._maps = null;
if (this.stash) {
this.stash.clear();
}
}
/**
*
*/
unset() {
this._type = 'unset';
}
/**
*
*/
isUnset() {
return this._type === 'unset';
}
/**
*
* JSExpresion | JSSlot
*/
isContainJSExpression(): boolean {
const type = this._type;
if (type === 'expression') {
return true;
}
if (type === 'literal' || type === 'unset') {
return false;
}
if ((type === 'list' || type === 'map') && this.items) {
return this.items.some(item => item.isContainJSExpression());
}
return false;
}
/**
* JSON
*/
isJSON() {
return !this.isContainJSExpression();
}
private _items: Prop[] | null = null;
private _maps: Map<string, Prop> | null = null;
@computed private get items(): Prop[] | null {
let _items: any;
untracked(() => {
_items = this._items;
});
if (!_items) {
if (this._type === 'list') {
const data = this._value;
const items = [];
for (const item of data) {
items.push(new Prop(this, item));
}
_items = items;
this._maps = null;
} else if (this._type === 'map') {
const data = this._value;
const items = [];
const maps = new Map<string, Prop>();
const keys = Object.keys(data);
for (const key of keys) {
const prop = new Prop(this, data[key], key);
items.push(prop);
maps.set(key, prop);
}
_items = items;
this._maps = maps;
} else {
_items = null;
this._maps = null;
}
this._items = _items;
}
return _items;
}
@computed private get maps(): Map<string, Prop> | null {
if (!this.items || this.items.length < 1) {
return null;
}
return this._maps;
}
private stash: StashSpace | undefined;
/**
*
*/
@obx key: string | number | undefined;
/**
*
*/
@obx spread: boolean;
constructor(
public parent: IPropParent,
value: CompositeValue | UNSET = UNSET,
key?: string | number,
spread = false,
) {
if (value !== UNSET) {
this.value = value;
}
this.key = key;
this.spread = spread;
}
/**
*
* @param stash
*/
get(path: string, stash: false): Prop | null;
/**
* ,
* @param stash
*/
get(path: string, stash: true): Prop;
/**
* ,
*/
get(path: string): Prop;
get(path: string, stash = true) {
const type = this._type;
if (type !== 'map' && type !== 'unset' && !stash) {
return null;
}
const maps = type === 'map' ? this.maps : null;
let prop: any = maps ? maps.get(path) : null;
if (prop) {
return prop;
}
const i = path.indexOf('.');
let entry = path;
let nest = '';
if (i > 0) {
nest = path.slice(i + 1);
if (nest) {
entry = path.slice(0, i);
prop = maps ? maps.get(entry) : null;
if (prop) {
return prop.get(nest, stash);
}
}
}
if (stash) {
if (!this.stash) {
this.stash = new StashSpace(
item => {
// item take effect
this.set(String(item.key), item);
item.parent = this;
},
() => {
return true;
},
);
}
prop = this.stash.get(entry);
if (nest) {
return prop.get(nest, true);
}
return prop;
}
return null;
}
/**
*
*/
remove() {
this.parent.delete(this);
}
/**
*
*/
delete(prop: Prop): void {
if (this.items) {
const i = this.items.indexOf(prop);
if (i > -1) {
this.items.slice(i, 1);
prop.purge();
}
if (this._maps && prop.key) {
this._maps.delete(String(prop.key));
}
}
}
/**
* key
*/
deleteKey(key: string): void {
if (this.maps) {
const prop = this.maps.get(key);
if (prop) {
this.delete(prop);
}
}
}
/**
*
*/
size(): number {
return this.items?.length || 0;
}
/**
*
*
* @param force
*/
add(value: CompositeValue, force = false): Prop | null {
const type = this._type;
if (type !== 'list' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'list')) {
this.value = [];
}
const prop = new Prop(this, value);
this.items!.push(prop);
return prop;
}
/**
*
*
* @param force
*/
set(key: string, value: CompositeValue | Prop, force = false) {
const type = this._type;
if (type !== 'map' && type !== 'unset' && !force) {
return null;
}
if (type === 'unset' || (force && type !== 'map')) {
this.value = {};
}
const prop = isProp(value) ? value : new Prop(this, value, key);
const items = this.items!;
const maps = this.maps!;
const orig = maps.get(key);
if (orig) {
// replace
const i = items.indexOf(orig);
if (i > -1) {
items.splice(i, 1, prop)[0].purge();
}
maps.set(key, prop);
} else {
// push
items.push(prop);
maps.set(key, prop);
}
return prop;
}
/**
* key
*/
has(key: string): boolean {
if (this._type !== 'map') {
return false;
}
if (this._maps) {
return this._maps.has(key);
}
return hasOwnProperty(this._value, key);
}
private purged = false;
/**
*
*/
purge() {
if (this.purged) {
return;
}
this.purged = true;
if (this.stash) {
this.stash.purge();
}
if (this._items) {
this._items.forEach(item => item.purge());
}
this._maps = null;
}
/**
*
*/
[Symbol.iterator](): { next(): { value: Prop } } {
let index = 0;
const items = this.items;
const length = items?.length || 0;
return {
next() {
if (index < length) {
return {
value: items![index++],
done: false,
};
}
return {
value: undefined as any,
done: true,
};
},
};
}
/**
*
*/
forEach(fn: (item: Prop, key: number | string | undefined) => void): void {
const items = this.items;
if (!items) {
return;
}
const isMap = this._type === 'map';
items.forEach((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
/**
*
*/
map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null {
const items = this.items;
if (!items) {
return null;
}
const isMap = this._type === 'map';
return items.map((item, index) => {
return isMap ? fn(item, item.key) : fn(item, index);
});
}
}
export function isProp(obj: any): obj is Prop {
return obj && obj.isProp;
}
export default class Props<T> implements IPropParent {
@obx.val private readonly items: Prop[] = [];
@obx.ref private get maps(): Map<string, Prop> {
const maps = new Map();
if (this.items.length > 0) {
this.items.forEach(prop => {
if (prop.key) {
maps.set(prop.key, prop);
}
});
}
return maps;
}
private stash = new StashSpace(
prop => {
this.items.push(prop);
prop.parent = this;
},
() => {
return true;
},
);
get size() {
return this.items.length;
}
private _type: 'map' | 'list' | 'unset' = 'unset';
constructor(owner: T, value?: PropsMap | PropsList | null) {
if (value == null) {
this._type = 'unset';
} else if (Array.isArray(value)) {
this._type = 'list';
value.forEach(item => {});
} else {
this._type = 'map';
}
}
delete(prop: Prop) {
const i = this.items.indexOf(prop);
if (i > -1) {
this.items.splice(i, 1);
prop.purge();
}
}
query(path: string, useStash = true) {
let matchedLength = 0;
let firstMatched = null;
if (this.items) {
// target: a.b.c
// trys: a.b.c, a.b, a
let i = this.items.length;
while (i-- > 0) {
const expr = this.items[i];
if (!expr.key) {
continue;
}
const name = String(expr.key);
if (name === path) {
// completely match
return expr;
}
// fisrt match
const l = name.length;
if (path.slice(0, l + 1) === `${name}.`) {
matchedLength = l;
firstMatched = expr;
}
}
}
let ret = null;
if (firstMatched) {
ret = firstMatched.get(path.slice(matchedLength + 1), true);
}
if (!ret && useStash) {
return this.stash.get(path);
}
return ret;
}
get(name: string, useStash = false) {
return this.maps.get(name) || (useStash && this.stash.get(name)) || null;
}
add(value: CompositeValue | null, key?: string | number, spread = false) {
const prop = new Prop(this, value, key, spread);
this.items.push(prop);
}
private purged = false;
purge() {
if (this.purged) {
return;
}
this.purged = true;
this.stash.purge();
this.items.forEach(item => item.purge());
}
}

View File

@ -0,0 +1,133 @@
import Node from './node';
/**
* state
* lifeCycles
* fileName
* meta
* methods
* dataSource
* css
* defaultProps
*/
export default class RootNode extends Node {
readonly isRootNode = true;
readonly index = 0;
readonly props: object = {};
readonly nextSibling = null;
readonly prevSibling = null;
readonly zLevel = 0;
readonly parent = null;
internalSetParent(parent: null) {}
get viewData(): ViewData {
return {
file: this.file,
children: this.nodeData,
};
}
readonly fileName: string;
readonly viewType: string;
readonly viewVersion: string;
get ready() {
return this.document.ready;
}
get nodeData(): NodeData[] {
if (!this.ready) {
// TODO: add mocks data
return this.childrenData;
}
const children = this.children;
if (!children || children.length < 1) {
return [];
}
return children.map(node => node.nodeData as NodeData);
}
private childrenData: NodeData[];
private _children: INode[] | null = null;
@obx.val get children(): INode[] {
if (this._children) {
return this._children;
}
if (!this.ready || this.purged) {
return [];
}
const children = this.childrenData;
/* eslint-disable */
this._children = children
? untracked(() =>
children.map(child => {
const node = this.document.createNode(child);
node.internalSetParent(this);
return node;
}),
)
: [];
/* eslint-enable */
return this._children;
}
get scope() {
return this.mocks.scope;
}
constructor(readonly document: DocumentContext, { children, file, viewType, viewVersion }: ViewData) {
this.file = file;
this.viewType = viewType || '';
this.viewVersion = viewVersion || '';
const expr = getMockExpr(children);
if (expr) {
this.childrenData = children.slice(0, -1);
this.mocksExpr = expr;
} else {
this.childrenData = children.slice();
}
}
merge(schema: DocumentSchema) {
for (let i = 0, l = data.length; i < l; i++) {
const item = this.children[i];
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
item.merge(data[i]);
} else {
if (item) {
item.purge();
}
this.children[i] = this.document.createNode(data[i]);
this.children[i].internalSetParent(this);
}
}
if (this.children.length > data.length) {
this.children.splice(data.length).forEach(child => child.purge());
}
}
// todo:
reuse() {}
private purged = false;
purge() {
if (this.purged) {
return;
}
this.purged = true;
if (this._children) {
this._children.forEach(child => child.purge());
}
}
receiveViewData({ children }: ViewData) {
this.merge(children);
// this.selection.dispose();
}
}
export function isRootNode(node: any): node is RootNode {
return node && node.isRootNode;
}

View File

@ -0,0 +1,134 @@
export class ScrollTarget {
get left() {
return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft;
}
get top() {
return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop;
}
scrollTo(options: { left?: number; top?: number }) {
this.target.scrollTo(options);
}
scrollToXY(x: number, y: number) {
this.target.scrollTo(x, y);
}
constructor(private target: Window | Element) {}
}
function easing(n: number) {
return Math.sin((n * Math.PI) / 2);
}
const SCROLL_ACCURCY = 30;
export default class Scroller {
private pid: number | undefined;
constructor(private board: { bounds: any }, private scrollTarget: ScrollTarget) {}
scrollTo(options: { left?: number; top?: number }) {
this.cancel();
let pid: number;
const left = this.scrollTarget.left;
const top = this.scrollTarget.top;
const end = () => {
this.cancel();
};
if ((left === options.left || options.left == null) && top === options.top) {
end();
return;
}
const duration = 200;
const start = +new Date();
const animate = () => {
if (pid !== this.pid) {
return;
}
const now = +new Date();
const time = Math.min(1, (now - start) / duration);
const eased = easing(time);
const opt: any = {};
if (options.left != null) {
opt.left = eased * (options.left - left) + left;
}
if (options.top != null) {
opt.top = eased * (options.top - top) + top;
}
this.scrollTarget.scrollTo(opt);
if (time < 1) {
this.pid = pid = requestAnimationFrame(animate);
} else {
end();
}
};
this.pid = pid = requestAnimationFrame(animate);
}
scrolling(point: { globalX: number; globalY: number }) {
this.cancel();
const x = point.globalX;
const y = point.globalY;
const bounds = this.board.bounds;
const scale = bounds.scale;
const maxScrollHeight = bounds.scrollHeight - bounds.height / scale;
const maxScrollWidth = bounds.scrollWidth - bounds.width / scale;
let sx = this.scrollTarget.left;
let sy = this.scrollTarget.top;
let ax = 0;
let ay = 0;
if (y < bounds.top + SCROLL_ACCURCY) {
ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale;
} else if (y > bounds.bottom - SCROLL_ACCURCY) {
ay = Math.min(Math.max(y + SCROLL_ACCURCY - bounds.bottom, 10), 50) / scale;
}
if (x < bounds.left + SCROLL_ACCURCY) {
ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale;
} else if (x > bounds.right - SCROLL_ACCURCY) {
ax = Math.min(Math.max(x + SCROLL_ACCURCY - bounds.right, 10), 50) / scale;
}
if (!ax && !ay) {
return;
}
const animate = () => {
let scroll = false;
if ((ay > 0 && sy < maxScrollHeight) || (ay < 0 && sy > 0)) {
sy += ay;
sy = Math.min(Math.max(sy, 0), maxScrollHeight);
scroll = true;
}
if ((ax > 0 && sx < maxScrollWidth) || (ax < 0 && sx > 0)) {
sx += ax;
sx = Math.min(Math.max(sx, 0), maxScrollWidth);
scroll = true;
}
if (!scroll) {
return;
}
this.scrollTarget.scrollTo({ left: sx, top: sy });
this.pid = requestAnimationFrame(animate);
};
animate();
}
cancel() {
if (this.pid) {
cancelAnimationFrame(this.pid);
}
this.pid = undefined;
}
}

View File

@ -0,0 +1,151 @@
import { INode, contains, isNode, comparePosition } from './node';
import { obx } from '@ali/recore';
import DocumentContext from './document-context';
export class Selection {
@obx.val private selected: string[] = [];
constructor(private doc: DocumentContext) {}
select(id: string) {
if (this.selected.length === 1 && this.selected.indexOf(id) > -1) {
// avoid cause reaction
return;
}
this.selected = [id];
}
selectAll(ids: string[]) {
this.selected = ids;
}
clear() {
this.selected = [];
}
dispose() {
let i = this.selected.length;
while (i-- > 0) {
const id = this.selected[i];
const node = this.doc.getNode(id, true);
if (!node) {
this.selected.splice(i, 1);
} else if (node.id !== id) {
this.selected[i] = id;
}
}
}
add(id: string) {
if (this.selected.indexOf(id) > -1) {
return;
}
const i = this.findIndex(id);
if (i > -1) {
this.selected.splice(i, 1);
}
this.selected.push(id);
}
private findIndex(id: string): number {
const ns = getNamespace(id);
const nsx = `${ns}:`;
return this.selected.findIndex(idx => {
return idx === ns || idx.startsWith(nsx);
});
}
has(id: string, variant = false) {
return this.selected.indexOf(id) > -1 || (variant && this.findIndex(id) > -1);
}
del(id: string, variant = false) {
let i = this.selected.indexOf(id);
if (i > -1) {
this.selected.splice(i, 1);
} else if (variant) {
i = this.findIndex(id);
this.selected.splice(i, 1);
}
}
containsNode(node: INode) {
for (const id of this.selected) {
const parent = this.doc.getNode(id);
if (parent && contains(parent, node)) {
return true;
}
}
return false;
}
getNodes() {
const nodes = [];
for (const id of this.selected) {
const node = this.doc.getNode(id, true);
if (node) {
nodes.push(node);
}
}
return nodes;
}
getOriginNodes(): INode[] {
const nodes: any[] = [];
for (const id of this.selected) {
const node = this.doc.getOriginNode(id);
if (node && !nodes.includes(node)) {
nodes.push(node);
}
}
return nodes;
}
/**
* get union items that at top level
*/
getTopNodes(origin?: boolean) {
const nodes = [];
for (const id of this.selected) {
const node = origin ? this.doc.getOriginNode(id) : this.doc.getNode(id);
if (!node) {
continue;
}
let i = nodes.length;
let isTop = true;
while (i-- > 0) {
const n = comparePosition(nodes[i], node);
// nodes[i] contains node
if (n === 16 || n === 0) {
isTop = false;
break;
}
// node contains nodes[i], delete nodes[i]
if (n === 8) {
nodes.splice(i, 1);
}
}
// node is top item, push to nodes
if (isTop) {
nodes.push(node);
}
}
return nodes;
}
}
function getNamespace(id: string) {
const i = id.indexOf(':');
if (i < 0) {
return id;
}
return id.substring(0, i);
}
export function isSelectable(obj: any): obj is INode {
return isNode(obj);
}

View File

@ -0,0 +1,65 @@
import { obx, autorun, untracked } from '@recore/obx';
import { Prop, IPropParent } from './props';
export type PendingItem = Prop[];
export default class StashSpace implements IPropParent {
@obx.val private space: Set<Prop> = new Set();
@obx.ref private get maps(): Map<string, Prop> {
const maps = new Map();
if (this.space.size > 0) {
this.space.forEach(prop => {
maps.set(prop.key, prop);
});
}
return maps;
}
private willPurge: () => void;
constructor(write: (item: Prop) => void, before: () => boolean) {
this.willPurge = autorun(() => {
if (this.space.size < 1) {
return;
}
const pending: Prop[] = [];
for (const prop of this.space) {
if (!prop.isUnset()) {
this.space.delete(prop);
pending.push(prop);
}
}
if (pending.length > 0) {
untracked(() => {
if (before()) {
for (const item of pending) {
write(item);
}
}
});
}
});
}
get(key: string): Prop {
let prop = this.maps.get(key);
if (!prop) {
prop = new Prop(this, null, key);
this.space.add(prop);
}
return prop;
}
delete(prop: Prop) {
this.space.delete(prop);
prop.purge();
}
clear() {
this.space.forEach(item => item.purge());
this.space.clear();
}
purge() {
this.willPurge();
this.space.clear();
}
}

View File

@ -0,0 +1,96 @@
import { obx } from '@ali/recore';
import { screen } from '../globals/screen';
import { Point } from './location';
import { ScrollTarget } from './scroller';
export type AutoFit = '100%';
export const AutoFit = '100%';
export default class Viewport {
private shell: HTMLDivElement | undefined;
scrollTarget: ScrollTarget | undefined;
get scale(): number {
if (this.width === AutoFit) {
return 1;
}
return screen.width / this.width;
}
get height(): number | AutoFit {
if (this.scale === 1) {
return AutoFit;
}
return screen.height / this.scale;
}
private _bounds: ClientRect | DOMRect | null = null;
get bounds(): ClientRect | DOMRect {
if (this._bounds) {
return this._bounds;
}
this._bounds = this.shell!.getBoundingClientRect();
requestAnimationFrame(() => {
this._bounds = null;
});
return this._bounds;
}
get innerBounds(): ClientRect | DOMRect {
const bounds = this.bounds;
const scale = this.scale;
const ret: any = {
top: 0,
left: 0,
x: 0,
y: 0,
width: bounds.width / scale,
height: bounds.height / scale,
};
ret.right = ret.width;
ret.bottom = ret.height;
return ret;
}
@obx.ref width: number | AutoFit = AutoFit;
@obx.ref scrollX = 0;
@obx.ref scrollY = 0;
setShell(shell: HTMLDivElement) {
this.shell = shell;
}
setScrollTarget(target: Window) {
this.scrollTarget = new ScrollTarget(target);
this.scrollX = this.scrollTarget.left;
this.scrollY = this.scrollTarget.top;
target.onscroll = () => {
this.scrollX = this.scrollTarget!.left;
this.scrollY = this.scrollTarget!.top;
};
}
toGlobalPoint(point: Point): Point {
if (!this.shell) {
return point;
}
const rect = this.shell.getBoundingClientRect();
return {
clientX: point.clientX * this.scale + rect.left,
clientY: point.clientY * this.scale + rect.top,
};
}
toLocalPoint(point: Point): Point {
if (!this.shell) {
return point;
}
const rect = this.shell.getBoundingClientRect();
return {
clientX: (point.clientX - rect.left) / this.scale,
clientY: (point.clientY - rect.top) / this.scale,
};
}
}

View File

@ -0,0 +1,349 @@
import { EventEmitter } from 'events';
import MasterBoard from '../document/master-board';
import Location from '../document/location';
import { INode } from '../document/node';
import { NodeData } from '../document/document-data';
import { getCurrentDocument } from './current';
import { obx } from '@ali/recore';
export interface LocateEvent {
readonly type: 'LocateEvent';
readonly clientX: number;
readonly clientY: number;
readonly globalX: number;
readonly globalY: number;
readonly originalEvent: MouseEvent;
readonly dragTarget: DragTarget;
target: Element | null;
fixed?: true;
}
export type DragTarget = NodesDragTarget | NodeDatasDragTarget | AnyDragTarget;
export enum DragTargetType {
Nodes = 'nodes',
NodeDatas = 'nodedatas',
}
export interface NodesDragTarget {
type: DragTargetType.Nodes;
nodes: INode[];
}
export function isNodesDragTarget(obj: any): obj is NodesDragTarget {
return obj && obj.type === DragTargetType.Nodes;
}
export interface NodeDatasDragTarget {
type: DragTargetType.NodeDatas;
data: NodeData[];
maps?: { [tagName: string]: string };
thumbnail?: string;
description?: string;
[extra: string]: any;
}
export function isNodeDatasDragTarget(obj: any): obj is NodeDatasDragTarget {
return obj && obj.type === DragTargetType.NodeDatas;
}
export interface AnyDragTarget {
type: string;
[key: string]: any;
}
export function isAnyDragTarget(obj: any): obj is AnyDragTarget {
return obj && obj.type !== DragTargetType.NodeDatas && obj.type !== DragTargetType.Nodes;
}
export interface ISenseAble {
id: string;
sensitive: boolean;
fixEvent(e: LocateEvent): LocateEvent;
locate(e: LocateEvent): Location | undefined;
isEnter(e: LocateEvent): boolean;
inRange(e: LocateEvent): boolean;
deactive(): void;
}
export function isLocateEvent(e: any): e is LocateEvent {
return e && e.type === 'LocateEvent';
}
const SHAKE_DISTANCE = 4;
/**
* mouse shake check
*/
export function isShaken(e1: MouseEvent, e2: MouseEvent): boolean {
if ((e1 as any).shaken) {
return true;
}
if (e1.target !== e2.target) {
return true;
}
return Math.pow(e1.clientY - e2.clientY, 2) + Math.pow(e1.clientX - e2.clientX, 2) > SHAKE_DISTANCE;
}
export function setShaken(e: any) {
e.shaken = true;
}
function getTopDocument(e: MouseEvent, local: Document) {
return e.view!.document === local ? null : document;
}
class Dragon {
private sensors: ISenseAble[] = [];
/**
* current actived sensor
*/
private _activeSensor: ISenseAble | undefined;
get activeSensor(): ISenseAble | undefined {
return this._activeSensor;
}
@obx.ref dragging = false;
private emitter = new EventEmitter();
private get master(): MasterBoard | undefined {
const doc = getCurrentDocument();
if (!doc) {
return undefined;
}
return doc.masterBoard;
}
from(shell: Element, boost: (e: MouseEvent) => DragTarget | null) {
const mousedown = (e: MouseEvent) => {
// ESC or RightClick
if (e.which === 3 || e.button === 2) {
return;
}
// Get a new node to be dragged
const dragTarget = boost(e);
if (!dragTarget) {
return;
}
this.boost(dragTarget, e);
};
shell.addEventListener('mousedown', mousedown as any);
return () => {
shell.removeEventListener('mousedown', mousedown as any);
};
}
/**
* dragTarget should be a INode | INode[] | NodeData | NodeData[]
*/
boost(dragTarget: DragTarget, boostEvent: MouseEvent) {
if (!this.master) {
return;
}
const master = this.master;
const doc = master.contentDocument;
const viewport = master.document.viewport;
const topDoc = getTopDocument(boostEvent, doc);
const newBie = dragTarget.type !== DragTargetType.Nodes;
let lastLocation: any = null;
let lastSensor: ISenseAble | undefined;
this.dragging = false;
master.setNativeSelection(false);
const checkesc = (e: KeyboardEvent) => {
if (e.keyCode === 27) {
lastLocation = null;
master.document.clearLocation();
over();
}
};
const checkcopy = (e: MouseEvent) => {
if (newBie || e.altKey || e.ctrlKey) {
master.setCopy(true);
} else {
master.setCopy(false);
}
};
const drag = (e: MouseEvent) => {
checkcopy(e);
const locateEvent = fixEvent(e);
const sensor = chooseSensor(locateEvent);
if (sensor) {
sensor.fixEvent(locateEvent);
lastLocation = sensor.locate(locateEvent);
} else {
master.document.clearLocation();
lastLocation = null;
}
this.emitter.emit('drag', locateEvent, lastLocation);
};
const dragstart = () => {
const locateEvent = fixEvent(boostEvent);
if (!newBie) {
chooseSensor(locateEvent);
}
master.setDragging(true);
// ESC cancel drag
doc.addEventListener('keydown', checkesc, false);
if (topDoc) {
topDoc.addEventListener('keydown', checkesc, false);
}
this.emitter.emit('dragstart', locateEvent);
};
const move = (e: MouseEvent) => {
if (this.dragging) {
drag(e);
return;
}
if (isShaken(boostEvent, e)) {
this.dragging = true;
setShaken(boostEvent);
dragstart();
drag(e);
}
};
const over = (e?: any) => {
if (lastSensor) {
lastSensor.deactive();
}
master.setNativeSelection(true);
let exception;
if (this.dragging) {
this.dragging = false;
try {
this.emitter.emit('dragend', { dragTarget, copy: master.isCopy() }, lastLocation);
} catch (ex) {
exception = ex;
}
}
master.releaseCursor();
doc.removeEventListener('mousemove', move, true);
doc.removeEventListener('mouseup', over, true);
doc.removeEventListener('mousedown', over, true);
doc.removeEventListener('keydown', checkesc, false);
doc.removeEventListener('keydown', checkcopy as any, false);
doc.removeEventListener('keyup', checkcopy as any, false);
if (topDoc) {
topDoc.removeEventListener('mousemove', move, true);
topDoc.removeEventListener('mouseup', over, true);
topDoc.removeEventListener('mousedown', over, true);
topDoc.removeEventListener('keydown', checkesc, false);
topDoc.removeEventListener('keydown', checkcopy as any, false);
topDoc.removeEventListener('keyup', checkcopy as any, false);
}
if (exception) {
throw exception;
}
};
const fixEvent = (e: MouseEvent): LocateEvent => {
if (isLocateEvent(e)) {
return e;
}
const evt: any = {
type: 'LocateEvent',
target: e.target,
dragTarget,
originalEvent: e,
};
if (e.view!.document === document) {
const l = viewport.toLocalPoint(e);
evt.clientX = l.clientX;
evt.clientY = l.clientY;
evt.globalX = e.clientX;
evt.globalY = e.clientY;
} else {
const g = viewport.toGlobalPoint(e);
evt.clientX = e.clientX;
evt.clientY = e.clientY;
evt.globalX = g.clientX;
evt.globalY = g.clientY;
}
return evt;
};
const sensors: ISenseAble[] = ([master] as any).concat(this.sensors);
const chooseSensor = (e: LocateEvent) => {
let sensor;
if (newBie && !lastLocation) {
sensor = sensors.find(s => s.sensitive && s.isEnter(e));
} else {
sensor = sensors.find(s => s.sensitive && s.inRange(e)) || lastSensor;
}
if (sensor !== lastSensor) {
if (lastSensor) {
lastSensor.deactive();
}
lastSensor = sensor;
}
if (sensor) {
sensor.fixEvent(e);
}
this._activeSensor = sensor;
return sensor;
};
doc.addEventListener('mousemove', move, true);
doc.addEventListener('mouseup', over, true);
doc.addEventListener('mousedown', over, true);
if (topDoc) {
topDoc.addEventListener('mousemove', move, true);
topDoc.addEventListener('mouseup', over, true);
topDoc.addEventListener('mousedown', over, true);
}
if (!newBie) {
doc.addEventListener('keydown', checkcopy as any, false);
doc.addEventListener('keyup', checkcopy as any, false);
if (topDoc) {
topDoc.addEventListener('keydown', checkcopy as any, false);
topDoc.addEventListener('keyup', checkcopy as any, false);
}
}
}
addSensor(sensor: any) {
this.sensors.push(sensor);
}
removeSensor(sensor: any) {
const i = this.sensors.indexOf(sensor);
if (i > -1) {
this.sensors.splice(i, 1);
}
}
onDragstart(func: (e: LocateEvent) => any) {
this.emitter.on('dragstart', func);
return () => {
this.emitter.removeListener('dragstart', func);
};
}
onDrag(func: (e: LocateEvent, location: Location) => any) {
this.emitter.on('drag', func);
return () => {
this.emitter.removeListener('drag', func);
};
}
onDragend(func: (x: { dragTarget: DragTarget; copy: boolean }, location: Location) => any) {
this.emitter.on('dragend', func);
return () => {
this.emitter.removeListener('dragend', func);
};
}
}
export const dragon = new Dragon();

View File

@ -0,0 +1,114 @@
import Hotkey, { isFormEvent } from '../utils/hotkey';
import { getCurrentDocument, getCurrentAdaptor } from './current';
import { isShadowNode } from '../document/node/shadow-node';
import { focusing } from './focusing';
import { INode, isElementNode, insertChildren } from '../document/node';
import { activeTracker } from './active-tracker';
import clipboard from '../utils/clipboard';
export const hotkey = new Hotkey();
// hotkey binding
hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc) {
return;
}
e.preventDefault();
const sel = doc.selection;
const topItems = sel.getTopNodes();
topItems.forEach(node => {
if (isShadowNode(node)) {
doc.removeNode(node.origin);
} else {
doc.removeNode(node);
}
});
sel.clear();
});
hotkey.bind('escape', (e: KeyboardEvent) => {
const currentFocus = focusing.current;
if (isFormEvent(e) || !currentFocus) {
return;
}
e.preventDefault();
currentFocus.esc();
});
function isHTMLTag(name: string) {
return /^[a-z]\w*$/.test(name);
}
function isIgnore(uri: string) {
return /^(\.|@(builtins|html|imported):)/.test(uri);
}
function generateMaps(node: INode | INode[], maps: any = {}) {
if (Array.isArray(node)) {
node.forEach(n => generateMaps(n, maps));
return maps;
}
if (isElementNode(node)) {
const { uri, tagName } = node;
if (uri && !isHTMLTag(tagName) && !isIgnore(uri)) {
maps[tagName] = uri;
}
generateMaps(node.children, maps);
}
return maps;
}
// command + c copy command + x cut
hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc || !(focusing.id === 'outline' || focusing.id === 'canvas')) {
return;
}
e.preventDefault();
const selected = doc.selection.getTopNodes(true);
if (!selected || selected.length < 1) return;
const maps = generateMaps(selected);
const nodesData = selected.map(item => item.nodeData);
const code = getCurrentAdaptor().viewDataToSource({
file: '',
children: nodesData as any,
});
clipboard.setData({ code, maps });
/*
const cutMode = action.indexOf('x') > 0;
if (cutMode) {
const parentNode = selected.getParent();
parentNode.select();
selected.remove();
}
*/
});
// command + v paste
hotkey.bind(['command+v', 'ctrl+v'], e => {
const doc = getCurrentDocument();
if (isFormEvent(e) || !doc) {
return;
}
clipboard.waitPasteData(e, data => {
if (data.code && data.maps) {
const adaptor = getCurrentAdaptor();
let nodesData = adaptor.parseToViewData(data.code, data.maps).children;
nodesData = doc.processDocumentData(nodesData, data.maps);
const { target, index } = doc.getSuitableInsertion();
const nodes = insertChildren(target, nodesData, index);
if (nodes) {
doc.selection.selectAll(nodes.map(o => o.id));
setTimeout(() => activeTracker.track(nodes[0]), 10);
}
}
});
});
hotkey.mount(window);

View File

View File

@ -0,0 +1,76 @@
import { obx } from '@recore/obx';
import { DocumentSchema, ProjectSchema } from './schema';
import { EventEmitter } from 'events';
export default class Project {
@obx documents: DocumentContext[];
displayMode: 'exclusive' | 'split'; // P2
private emitter = new EventEmitter();
private data: ProjectSchema = {};
constructor(schema: ProjectSchema) {
this.data = { ...schema };
}
getDocument(fileName: string): DocumentContext {}
addDocument(data: DocumentSchema): DocumentContext {
this.documents.push(new DocumentContext(data));
}
/**
* schema
*/
getSchema(): ProjectSchema {
return {
...this.data,
componentsTree: this.documents.map(doc => doc.getSchema()),
};
}
/**
* schema
*/
setSchema(schema: ProjectSchema): void {}
/**
*
*/
set(
key:
| 'version'
| 'componentsTree'
| 'componentsMap'
| 'utils'
| 'constants'
| 'i18n'
| 'css'
| 'dataSource'
| string,
value: any,
): void {}
/**
*
*/
get(
key:
| 'version'
| 'componentsTree'
| 'componentsMap'
| 'utils'
| 'constants'
| 'i18n'
| 'css'
| 'dataSource'
| string,
): any;
edit(document): void {}
/**
* documents
*/
onDocumentsChange(fn: (documents: DocumentContext[]) => void): () => void {}
/**
*
*/
}

View File

@ -0,0 +1,159 @@
// 表达式
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 interface JSExpression {
type: 'JSExpression';
value: string;
}
export function isJSExpression(data: any): data is JSExpression {
return data && data.type === 'JSExpression';
}
export function isDOMText(data: any): data is DOMText {
return typeof data === 'string';
}
export type DOMText = string;
export interface RootSchema extends NodeSchema {
componentName: 'Block' | 'Page' | 'Component';
fileName: string;
meta?: object;
state?: {
[key: string]: CompositeValue;
};
methods?: {
[key: string]: JSExpression;
};
lifeCycles?: {
[key: string]: JSExpression;
};
css?: string;
dataSource?: {
items: DataSourceConfig[];
};
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

View File

@ -0,0 +1,95 @@
function getDataFromPasteEvent(event: ClipboardEvent) {
const clipboardData = event.clipboardData;
if (!clipboardData) {
return null;
}
try {
return JSON.parse(clipboardData.getData('text/plain'));
} catch (error) {
/*
const html = clipboardData.getData('text/html');
if (html !== '') {
// TODO: clear the html
return {
code: '<div dangerouslySetInnerHTML={ __html: html } />',
maps: {},
};
}
*/
// paste the text by div
return {
code: clipboardData.getData('text/plain'),
maps: {},
};
}
}
class Clipboard {
private copyPasters: HTMLTextAreaElement[] = [];
private waitFn?: (data: any, e: ClipboardEvent) => void;
isCopyPasteEvent(e: Event) {
this.isCopyPaster(e.target);
}
isCopyPaster(el: any) {
return this.copyPasters.includes(el);
}
initCopyPaster(el: HTMLTextAreaElement) {
this.copyPasters.push(el);
const onPaste = (e: ClipboardEvent) => {
if (this.waitFn) {
this.waitFn(getDataFromPasteEvent(e), e);
this.waitFn = undefined;
}
el.blur();
};
el.addEventListener('paste', onPaste, false);
return () => {
el.removeEventListener('paste', onPaste, false);
const i = this.copyPasters.indexOf(el);
if (i > -1) {
this.copyPasters.splice(i, 1);
}
};
}
injectCopyPaster(document: Document) {
const copyPaster = document.createElement<'textarea'>('textarea');
copyPaster.style.cssText = 'position: relative;left: -9999px;';
document.body.appendChild(copyPaster);
const dispose = this.initCopyPaster(copyPaster);
return () => {
dispose();
document.removeChild(copyPaster);
};
}
setData(data: any) {
const copyPaster = this.copyPasters.find(x => x.ownerDocument);
if (!copyPaster) {
return;
}
copyPaster.value = typeof data === 'string' ? data : JSON.stringify(data);
copyPaster.select();
copyPaster.ownerDocument!.execCommand('copy');
copyPaster.blur();
}
waitPasteData(e: KeyboardEvent, cb: (data: any, e: ClipboardEvent) => void) {
const win = e.view;
if (!win) {
return;
}
const copyPaster = this.copyPasters.find(cp => cp.ownerDocument === win.document);
if (copyPaster) {
copyPaster.select();
this.waitFn = cb;
}
}
}
export default new Clipboard();

View File

@ -0,0 +1,23 @@
import { isPlainObject } from './is-plain-object';
export function cloneDeep(src: any): any {
const type = typeof src;
let data: any;
if (src === null || src === undefined) {
data = src;
} else if (Array.isArray(src)) {
data = src.map(item => cloneDeep(item));
} else if (type === 'object' && isPlainObject(src)) {
data = {};
for (const key in src) {
if (src.hasOwnProperty(key)) {
data[key] = cloneDeep(src[key]);
}
}
} else {
data = src;
}
return data;
}

View File

@ -0,0 +1,17 @@
import { ReactNode, ComponentType, isValidElement, cloneElement, createElement, ReactElement } from 'react';
import { isReactClass } from './is-react';
export function createContent(content: ReactNode | ComponentType<any>, props?: object): ReactNode {
if (isValidElement(content)) {
return props ? cloneElement(content, props) : content;
}
if (isReactClass(content)) {
return createElement(content, props);
}
if (typeof content === 'function') {
return content(props) as ReactElement;
}
return content;
}

View File

@ -0,0 +1,17 @@
export interface Defer<T = any> {
resolve(value?: T | PromiseLike<T>): void;
reject(reason?: any): void;
promise(): Promise<T>;
}
export function createDefer<T = any>(): Defer<T> {
const r: any = {};
const promise = new Promise<T>((resolve, reject) => {
r.resolve = resolve;
r.reject = reject;
});
r.promise = () => promise;
return r;
}

View File

@ -0,0 +1,15 @@
html.my-cursor-dragging, html.my-cursor-dragging * {
cursor: move !important
}
html.my-cursor-x-resizing, html.my-cursor-x-resizing * {
cursor: col-resize;
}
html.my-cursor-y-resizing, html.my-cursor-y-resizing * {
cursor: row-resize;
}
html.my-cursor-copy, html.my-cursor-copy * {
cursor: copy !important
}

View File

@ -0,0 +1,60 @@
import './cursor.less';
export class Cursor {
private states = new Set<string>();
setDragging(flag: boolean) {
if (flag) {
this.addState('dragging');
} else {
this.removeState('dragging');
}
}
setXResizing(flag: boolean) {
if (flag) {
this.addState('x-resizing');
} else {
this.removeState('x-resizing');
}
}
setYResizing(flag: boolean) {
if (flag) {
this.addState('y-resizing');
} else {
this.removeState('y-resizing');
}
}
setCopy(flag: boolean) {
if (flag) {
this.addState('copy');
} else {
this.removeState('copy');
}
}
isCopy() {
return this.states.has('copy');
}
release() {
for (const state of this.states) {
this.removeState(state);
}
}
private addState(state: string) {
if (!this.states.has(state)) {
this.states.add(state);
document.documentElement.classList.add(`my-cursor-${state}`);
}
}
private removeState(state: string) {
if (this.states.has(state)) {
this.states.delete(state);
document.documentElement.classList.remove(`my-cursor-${state}`);
}
}
}
export default new Cursor();

View File

@ -0,0 +1,19 @@
export function isDOMNode(node: any): node is Element | Text {
return node.nodeType && (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE);
}
export function isElement(node: any): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
// a range for test TextNode clientRect
const cycleRange = document.createRange();
export function getClientRects(node: Element | Text) {
if (isElement(node)) {
return [node.getBoundingClientRect()];
}
cycleRange.selectNode(node);
return Array.from(cycleRange.getClientRects());
}

View File

@ -0,0 +1,7 @@
export function getPrototypeOf(target: any) {
if (typeof Object.getPrototypeOf !== 'undefined') {
return Object.getPrototypeOf(target);
}
return target.__proto__;
}

View File

@ -0,0 +1,4 @@
const prototypeHasOwnProperty = Object.prototype.hasOwnProperty;
export function hasOwnProperty(obj: any, key: string | number | symbol): boolean {
return obj && prototypeHasOwnProperty.call(obj, key);
}

View File

@ -0,0 +1,618 @@
interface KeyMap {
[key: number]: string;
}
interface CtrlKeyMap {
[key: string]: string;
}
interface ActionEvent {
type: string;
}
interface HotkeyCallbacks {
[key: string]: HotkeyCallbackCfg[];
}
interface HotkeyDirectMap {
[key: string]: HotkeyCallback;
}
export type HotkeyCallback = (e: KeyboardEvent, combo?: string) => any | false;
interface HotkeyCallbackCfg {
callback: HotkeyCallback;
modifiers: string[];
action: string;
seq?: string;
level?: number;
combo?: string;
}
interface KeyInfo {
key: string;
modifiers: string[];
action: string;
}
interface SequenceLevels {
[key: string]: number;
}
const MAP: KeyMap = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta',
};
const KEYCODE_MAP: KeyMap = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: "'",
};
const SHIFT_MAP: CtrlKeyMap = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
$: '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
_: '-',
'+': '=',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\',
};
const SPECIAL_ALIASES: CtrlKeyMap = {
option: 'alt',
command: 'meta',
return: 'enter',
escape: 'esc',
plus: '+',
mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl',
};
let REVERSE_MAP: CtrlKeyMap;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (let i = 1; i < 20; ++i) {
MAP[111 + i] = 'f' + i;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (let i = 0; i <= 9; ++i) {
MAP[i + 96] = String(i);
}
/**
* takes the event and returns the key character
*/
function characterFromEvent(e: KeyboardEvent): string {
const keyCode = e.keyCode || e.which;
// for keypress events we should return the character as is
if (e.type === 'keypress') {
let character = String.fromCharCode(keyCode);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (MAP[keyCode]) {
return MAP[keyCode];
}
if (KEYCODE_MAP[keyCode]) {
return KEYCODE_MAP[keyCode];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
return String.fromCharCode(keyCode).toLowerCase();
}
interface KeypressEvent extends KeyboardEvent {
type: 'keypress';
}
function isPressEvent(e: KeyboardEvent | ActionEvent): e is KeypressEvent {
return e.type === 'keypress';
}
export function isFormEvent(e: KeyboardEvent) {
const t = e.target as HTMLFormElement;
if (!t) {
return false;
}
if (t.form || /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)) {
return true;
}
if (/write/.test(window.getComputedStyle(t).getPropertyValue('-webkit-user-modify'))) {
return true;
}
return false;
}
/**
* checks if two arrays are equal
*/
function modifiersMatch(modifiers1: string[], modifiers2: string[]): boolean {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* takes a key event and figures out what the modifiers are
*/
function eventModifiers(e: KeyboardEvent): string[] {
const modifiers = [];
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
return modifiers;
}
/**
* determines if the keycode specified is a modifier key or not
*/
function isModifier(key: string): boolean {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function getReverseMap(): CtrlKeyMap {
if (!REVERSE_MAP) {
REVERSE_MAP = {};
for (const key in MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (Number(key) > 95 && Number(key) < 112) {
continue;
}
if (MAP.hasOwnProperty(key)) {
REVERSE_MAP[MAP[key]] = key;
}
}
}
return REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*/
function pickBestAction(key: string, modifiers: string[], action?: string): string {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action === 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function keysFromString(combination: string): string[] {
if (combination === '+') {
return ['+'];
}
combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* Gets info for a specific key combination
*
* @param combination key combination ("command+s" or "a" or "*")
*/
function getKeyInfo(combination: string, action?: string): KeyInfo {
let keys: string[] = [];
let key = '';
let i: number;
const modifiers: string[] = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (SPECIAL_ALIASES[key]) {
key = SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action !== 'keypress' && SHIFT_MAP[key]) {
key = SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = pickBestAction(key, modifiers, action);
return {
key,
modifiers,
action,
};
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*/
function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string, sequence?: string): void {
if (callback(e, combo) === false) {
e.preventDefault();
e.stopPropagation();
}
}
export default class Hotkey {
private callBacks: HotkeyCallbacks = {};
private directMap: HotkeyDirectMap = {};
private sequenceLevels: SequenceLevels = {};
private resetTimer = 0;
private ignoreNextKeyup: boolean | string = false;
private ignoreNextKeypress = false;
private nextExpectedAction: boolean | string = false;
mount(window: Window) {
const document = window.document;
const handleKeyEvent = this.handleKeyEvent.bind(this);
document.addEventListener('keypress', handleKeyEvent, false);
document.addEventListener('keydown', handleKeyEvent, false);
document.addEventListener('keyup', handleKeyEvent, false);
return () => {
document.removeEventListener('keypress', handleKeyEvent, false);
document.removeEventListener('keydown', handleKeyEvent, false);
document.removeEventListener('keyup', handleKeyEvent, false);
};
}
bind(combos: string[] | string, callback: HotkeyCallback, action?: string): Hotkey {
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
return this;
}
/**
* resets all sequence counters except for the ones passed in
*/
private resetSequences(doNotReset?: SequenceLevels): void {
// doNotReset = doNotReset || {};
let activeSequences = false;
let key = '';
for (key in this.sequenceLevels) {
if (doNotReset && doNotReset[key]) {
activeSequences = true;
} else {
this.sequenceLevels[key] = 0;
}
}
if (!activeSequences) {
this.nextExpectedAction = false;
}
}
/**
* finds all callbacks that match based on the keycode, modifiers,
* and action
*/
private getMatches(
character: string,
modifiers: string[],
e: KeyboardEvent | ActionEvent,
sequenceName?: string,
combination?: string,
level?: number,
): HotkeyCallbackCfg[] {
let i: number;
let callback: HotkeyCallbackCfg;
const matches: HotkeyCallbackCfg[] = [];
const action: string = e.type;
// if there are no events related to this keycode
if (!this.callBacks[character]) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if (action === 'keyup' && isModifier(character)) {
modifiers = [character];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < this.callBacks[character].length; ++i) {
callback = this.callBacks[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if (action !== callback.action) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if ((isPressEvent(e) && !e.metaKey && !e.ctrlKey) || modifiersMatch(modifiers, callback.modifiers)) {
const deleteCombo = !sequenceName && callback.combo === combination;
const deleteSequence = sequenceName && callback.seq === sequenceName && callback.level === level;
if (deleteCombo || deleteSequence) {
this.callBacks[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
}
private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void {
const callbacks: HotkeyCallbackCfg[] = this.getMatches(character, modifiers, e);
let i: number;
const doNotReset: SequenceLevels = {};
let maxLevel = 0;
let processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
if (callbacks[i].seq) {
maxLevel = Math.max(maxLevel, callbacks[i].level || 0);
}
}
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if (callbacks[i].seq) {
// only fire callbacks for the maxLevel to prevent
// subsequences from also firing
//
// for example 'a option b' should not cause 'option b' to fire
// even though 'option b' is part of the other sequence
//
// any sequences that do not match here will be discarded
// below by the resetSequences call
if (callbacks[i].level !== maxLevel) {
continue;
}
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq || ''] = 1;
fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if (!processedSequenceCallback) {
fireCallback(callbacks[i].callback, e, callbacks[i].combo);
}
}
const ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
this.resetSequences(doNotReset);
}
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
}
private handleKeyEvent(e: KeyboardEvent): void {
const character = characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type === 'keyup' && this.ignoreNextKeyup === character) {
this.ignoreNextKeyup = false;
return;
}
this.handleKey(character, eventModifiers(e), e);
}
private resetSequenceTimer(): void {
if (this.resetTimer) {
clearTimeout(this.resetTimer);
}
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
}
private bindSequence(combo: string, keys: string[], callback: HotkeyCallback, action?: string): void {
// const self: any = this;
this.sequenceLevels[combo] = 0;
const increaseSequence = (nextAction: string) => {
return () => {
this.nextExpectedAction = nextAction;
++this.sequenceLevels[combo];
this.resetSequenceTimer();
};
};
const callbackAndReset = (e: KeyboardEvent): void => {
fireCallback(callback, e, combo);
if (action !== 'keyup') {
this.ignoreNextKeyup = characterFromEvent(e);
}
setTimeout(this.resetSequences, 10);
};
for (let i = 0; i < keys.length; ++i) {
const isFinal = i + 1 === keys.length;
const wrappedCallback = isFinal ? callbackAndReset : increaseSequence(action || getKeyInfo(keys[i + 1]).action);
this.bindSingle(keys[i], wrappedCallback, action, combo, i);
}
}
private bindSingle(
combination: string,
callback: HotkeyCallback,
action?: string,
sequenceName?: string,
level?: number,
): void {
// store a direct mapped reference for use with HotKey.trigger
this.directMap[`${combination}:${action}`] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
const sequence: string[] = combination.split(' ');
let info: KeyInfo;
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
this.bindSequence(combination, sequence, callback, action);
return;
}
info = getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
this.callBacks[info.key] = this.callBacks[info.key] || [];
// remove an existing match if there is one
this.getMatches(info.key, info.modifiers, { type: info.action }, sequenceName, combination, level);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level,
combo: combination,
});
}
private bindMultiple(combinations: string[], callback: HotkeyCallback, action?: string) {
for (const item of combinations) {
this.bindSingle(item, callback, action);
}
}
}

View File

@ -0,0 +1,9 @@
export * from './is-object';
export * from './is-plain-object';
export * from './has-own-property';
export * from './set-prototype-of';
export * from './get-prototype-of';
export * from './shallow-equal';
export * from './clone-deep';
export * from './throttle';
export * from './unique-id';

View File

@ -0,0 +1,3 @@
export function isCSSUrl(url: string): boolean {
return /\.css$/.test(url);
}

View File

@ -0,0 +1,3 @@
export function isESModule(obj: any): obj is { [key: string]: any } {
return obj && obj.__esModule;
}

View File

@ -0,0 +1,3 @@
export function isFunction(fn: any): boolean {
return typeof fn === 'function';
}

View File

@ -0,0 +1,3 @@
export function isObject(value: any): value is object {
return value !== null && typeof value === 'object';
}

View File

@ -0,0 +1,9 @@
import { isObject } from './is-object';
export function isPlainObject(value: any) {
if (!isObject(value)) {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null;
}

View File

@ -0,0 +1,9 @@
import { ComponentClass, Component, ComponentType } from 'react';
export function isReactClass(obj: any): obj is ComponentClass<any> {
return obj && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component);
}
export function isReactComponent(obj: any): obj is ComponentType<any> {
return obj && (isReactClass(obj) || typeof obj === 'function');
}

View File

@ -0,0 +1,7 @@
export function parseCode(code: string): string {
try {
return JSON.parse(code);
} catch (e) {
return code;
}
}

View File

@ -0,0 +1,173 @@
/**
* Check whether a component is external package, e.g. @ali/uxcore
* @param path Component path
*/
export function isPackagePath(path: string): boolean {
return !path.startsWith('.') && !path.startsWith('/');
}
/**
* Title cased string
* @param s original string
*/
export function toTitleCase(s: string): string {
return s
.split(/[-_ .]+/)
.map(token => token[0].toUpperCase() + token.substring(1))
.join('');
}
/**
* Make up an import name/tag for components
* @param path Original path name
*/
export function generateComponentName(path: string): string {
const parts = path.split('/');
let name = parts.pop();
if (name && /^index\./.test(name)) {
name = parts.pop();
}
return name ? toTitleCase(name) : 'Component';
}
/**
* normalizing import path for easier comparison
*/
export function getNormalizedImportPath(path: string): string {
const segments = path.split('/');
let basename = segments.pop();
if (!basename) {
return path;
}
const ignoredExtensions = ['.ts', '.js', '.tsx', '.jsx'];
const extIndex = basename.lastIndexOf('.');
if (extIndex > -1) {
const ext = basename.slice(extIndex);
if (ignoredExtensions.includes(ext)) {
basename = basename.slice(0, extIndex);
}
}
if (basename !== 'index') {
segments.push(basename);
}
return segments.join('/');
}
/**
* make a relative path
*
* @param toPath abolute path
* @param fromPath absolute path
*/
export function makeRelativePath(toPath: string, fromPath: string) {
// not a absolute path, eg. @ali/uxcore
if (!toPath.startsWith('/')) {
return toPath;
}
const toParts = toPath.split('/');
const fromParts = fromPath.split('/');
// find shared path header
const length = Math.min(fromParts.length, toParts.length);
let sharedUpTo = length;
for (let i = 0; i < length; i++) {
if (fromParts[i] !== toParts[i]) {
sharedUpTo = i;
break;
}
}
// find how many levels to go up from
// minus another 1 since we do not include the final
const numGoUp = fromParts.length - sharedUpTo - 1;
// generate final path
let outputParts = [];
if (numGoUp === 0) {
// in the same dir
outputParts.push('.');
} else {
// needs to go up
for (let i = 0; i < numGoUp; ++i) {
outputParts.push('..');
}
}
outputParts = outputParts.concat(toParts.slice(sharedUpTo));
return outputParts.join('/');
}
function normalizeArray(parts: string[], allowAboveRoot: boolean) {
const res = [];
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
// ignore empty parts
if (!p || p === '.') {
continue;
}
if (p === '..') {
if (res.length && res[res.length - 1] !== '..') {
res.pop();
} else if (allowAboveRoot) {
res.push('..');
}
} else {
res.push(p);
}
}
return res;
}
function normalize(path: string): string {
const isAbsolute = path[0] === '/';
const segments = normalizeArray(path.split('/'), !isAbsolute);
if (isAbsolute) {
segments.unshift('');
} else if (segments.length < 1 || segments[0] !== '..') {
segments.unshift('.');
}
return segments.join('/');
}
/**
* Resolve component with absolute path to relative path
* @param path absolute path of component from project
*/
export function resolveAbsoluatePath(path: string, base: string): string {
if (!path.startsWith('.')) {
// eg. /usr/path/to, @ali/button
return path;
}
path = path.replace(/\\/g, '/');
if (base.slice(-1) !== '/') {
base += '/';
}
return normalize(base + path);
}
export function joinPath(...segments: string[]) {
let path = '';
for (const seg of segments) {
if (seg) {
if (path === '') {
path += seg;
} else {
path += '/' + seg;
}
}
}
return normalize(path);
}
export function removeVersion(path: string): string {
if (path.lastIndexOf('@') > 0) {
path = path.replace(/(@?[^@]+)(@[\w.-]+)(.+)/, '$1$3');
}
return path;
}

View File

@ -0,0 +1,32 @@
import { ReactInstance } from 'react';
import { isDOMNode, isElement } from './dom';
const FIBER_KEY = '_reactInternalFiber';
function elementsFromFiber(fiber: any, elements: Array<Element | Text>) {
if (fiber) {
if (fiber.stateNode && isDOMNode(fiber.stateNode)) {
elements.push(fiber.stateNode);
} else if (fiber.child) {
// deep fiberNode.child
elementsFromFiber(fiber.child, elements);
}
if (fiber.sibling) {
elementsFromFiber(fiber.sibling, elements);
}
}
}
export function findDOMNodes(elem: Element | ReactInstance | null): Array<Element | Text> | null {
if (!elem) {
return null;
}
if (isElement(elem)) {
return [elem];
}
const elements: Array<Element | Text> = [];
const fiberNode = (elem as any)[FIBER_KEY];
elementsFromFiber(fiberNode.child, elements);
return elements.length > 0 ? elements : null;
}

View File

@ -0,0 +1,38 @@
import { createDefer } from './create-defer';
export function evaluate(script: string) {
const scriptEl = document.createElement('script');
scriptEl.text = script;
document.head.appendChild(scriptEl);
document.head.removeChild(scriptEl);
}
export function load(url: string) {
const node: any = document.createElement('script');
// node.setAttribute('crossorigin', 'anonymous');
node.onload = onload;
node.onerror = onload;
const i = createDefer();
function onload(e: any) {
node.onload = null;
node.onerror = null;
if (e.type === 'load') {
i.resolve();
} else {
i.reject();
}
// document.head.removeChild(node);
// node = null;
}
// node.async = true;
node.src = url;
document.head.appendChild(node);
return i.promise();
}

View File

@ -0,0 +1,8 @@
export function setPrototypeOf(target: any, proto: any) {
// tslint:disable-next-line
if (typeof Object.setPrototypeOf !== 'undefined') {
Object.setPrototypeOf(target, proto); // tslint:disable-line
} else {
target.__proto__ = proto;
}
}

View File

@ -0,0 +1,27 @@
import { hasOwnProperty } from './has-own-property';
export function shallowEqual(objA: any, objB: any): boolean {
if (objA === objB) {
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (!hasOwnProperty(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,55 @@
export default class StylePoint {
private lastContent: string | undefined;
private lastUrl: string | undefined;
placeholder: Element | Text;
next: StylePoint | null = null;
prev: StylePoint | null = null;
constructor(readonly id: string, readonly level: number, placeholder?: Element) {
if (placeholder) {
this.placeholder = placeholder;
} else {
this.placeholder = document.createTextNode('');
}
}
insert() {
if (this.next) {
document.head.insertBefore(this.placeholder, this.next.placeholder);
} else if (this.prev) {
document.head.insertBefore(this.placeholder, this.prev.placeholder.nextSibling);
} else {
document.head.appendChild(this.placeholder);
}
}
applyText(content: string) {
if (this.lastContent === content) {
return;
}
this.lastContent = content;
this.lastUrl = undefined;
const element = document.createElement('style');
element.setAttribute('type', 'text/css');
element.setAttribute('data-for', this.id);
element.appendChild(document.createTextNode(content));
document.head.insertBefore(element, this.placeholder);
document.head.removeChild(this.placeholder);
this.placeholder = element;
}
applyUrl(url: string) {
if (this.lastUrl === url) {
return;
}
this.lastContent = undefined;
this.lastUrl = url;
const element = document.createElement('link');
element.href = url;
element.rel = 'stylesheet';
element.setAttribute('data-for', this.id);
document.head.insertBefore(element, this.placeholder);
document.head.removeChild(this.placeholder);
this.placeholder = element;
}
}

View File

@ -0,0 +1,100 @@
const useRAF = typeof requestAnimationFrame === 'function';
export function throttle(func: Function, delay: number) {
let lastArgs: any;
let lastThis: any;
let result: any;
let timerId: number | undefined;
let lastCalled: number | undefined;
let lastInvoked = 0;
function invoke(time: number) {
const args = lastArgs;
const thisArg = lastThis;
lastArgs = undefined;
lastThis = undefined;
lastInvoked = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc: any, wait: number): number {
if (useRAF) {
return requestAnimationFrame(pendingFunc);
}
return setTimeout(pendingFunc, wait) as any;
}
function leadingEdge(time: number) {
lastInvoked = time;
timerId = startTimer(timerExpired, delay);
return invoke(time);
}
function shouldInvoke(time: number) {
const timeSinceLastCalled = time - lastCalled!;
const timeSinceLastInvoked = time - lastInvoked;
return (
lastCalled === undefined ||
timeSinceLastCalled >= delay ||
timeSinceLastCalled < 0 ||
timeSinceLastInvoked >= delay
);
}
function remainingWait(time: number) {
const timeSinceLastCalled = time - lastCalled!;
const timeSinceLastInvoked = time - lastInvoked;
return Math.min(delay - timeSinceLastCalled, delay - timeSinceLastInvoked);
}
function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = startTimer(timerExpired, remainingWait(time));
}
function trailingEdge(time: number) {
timerId = undefined;
if (lastArgs) {
return invoke(time);
}
lastArgs = undefined;
lastThis = undefined;
return result;
}
function debounced(this: any, ...args: any[]) {
const time = Date.now();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
lastCalled = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCalled);
}
timerId = startTimer(timerExpired, delay);
return invoke(lastCalled);
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, delay);
}
return result;
}
return debounced;
}

View File

@ -0,0 +1,11 @@
import { Component, isValidElement } from 'react';
export function testReactType(obj: any) {
const t = typeof obj;
if (t === 'function' && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component)) {
return 'ReactClass';
} else if (t === 'object' && isValidElement(obj)) {
return 'ReactElement';
}
return t;
}

View File

@ -0,0 +1,4 @@
let guid = Date.now();
export function uniqueId(prefix = '') {
return `${prefix}${(guid++).toString(36).toLowerCase()}`;
}

View File

@ -0,0 +1,232 @@
function propertyNameRequiresQuotes(propertyName: string) {
try {
const context = {
worksWithoutQuotes: false,
};
new Function('ctx', `ctx.worksWithoutQuotes = {${propertyName}: true}['${propertyName}']`)();
return !context.worksWithoutQuotes;
} catch (ex) {
return true;
}
}
function quoteString(str: string, { doubleQuote }: any) {
return doubleQuote ? `"${str.replace(/"/gu, '\\"')}"` : `'${str.replace(/'/gu, "\\'")}'`;
}
export function valueToSource(
value: any,
{
circularReferenceToken = 'CIRCULAR_REFERENCE',
doubleQuote = true,
includeFunctions = true,
includeUndefinedProperties = false,
indentLevel = 0,
indentString = ' ',
lineEnding = '\n',
visitedObjects = new Set(),
}: any = {},
): any {
switch (typeof value) {
case 'boolean':
return value ? `${indentString.repeat(indentLevel)}true` : `${indentString.repeat(indentLevel)}false`;
case 'function':
if (includeFunctions) {
return `${indentString.repeat(indentLevel)}${value}`;
}
return null;
case 'number':
return `${indentString.repeat(indentLevel)}${value}`;
case 'object':
if (!value) {
return `${indentString.repeat(indentLevel)}null`;
}
if (visitedObjects.has(value)) {
return `${indentString.repeat(indentLevel)}${circularReferenceToken}`;
}
if (value instanceof Date) {
return `${indentString.repeat(indentLevel)}new Date(${quoteString(value.toISOString(), {
doubleQuote,
})})`;
}
if (value instanceof Map) {
return value.size
? `${indentString.repeat(indentLevel)}new Map(${valueToSource([...value], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
}).substr(indentLevel * indentString.length)})`
: `${indentString.repeat(indentLevel)}new Map()`;
}
if (value instanceof RegExp) {
return `${indentString.repeat(indentLevel)}/${value.source}/${value.flags}`;
}
if (value instanceof Set) {
return value.size
? `${indentString.repeat(indentLevel)}new Set(${valueToSource([...value], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
}).substr(indentLevel * indentString.length)})`
: `${indentString.repeat(indentLevel)}new Set()`;
}
if (Array.isArray(value)) {
if (!value.length) {
return `${indentString.repeat(indentLevel)}[]`;
}
const itemsStayOnTheSameLine = value.every(
item =>
typeof item === 'object' &&
item &&
!(item instanceof Date) &&
!(item instanceof Map) &&
!(item instanceof RegExp) &&
!(item instanceof Set) &&
(Object.keys(item).length || value.length === 1),
);
let previousIndex: number | null = null;
value = value.reduce((items, item, index) => {
if (previousIndex !== null) {
for (let i = index - previousIndex - 1; i > 0; i -= 1) {
items.push(indentString.repeat(indentLevel + 1));
}
}
previousIndex = index;
item = valueToSource(item, {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel: itemsStayOnTheSameLine ? indentLevel : indentLevel + 1,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
});
if (item === null) {
items.push(indentString.repeat(indentLevel + 1));
} else if (itemsStayOnTheSameLine) {
items.push(item.substr(indentLevel * indentString.length));
} else {
items.push(item);
}
return items;
}, []);
return itemsStayOnTheSameLine
? `${indentString.repeat(indentLevel)}[${value.join(', ')}]`
: `${indentString.repeat(indentLevel)}[${lineEnding}${value.join(
`,${lineEnding}`,
)}${lineEnding}${indentString.repeat(indentLevel)}]`;
}
value = Object.keys(value).reduce<string[]>((entries, propertyName) => {
const propertyValue = value[propertyName],
propertyValueString =
typeof propertyValue !== 'undefined' || includeUndefinedProperties
? valueToSource(value[propertyName], {
circularReferenceToken,
doubleQuote,
includeFunctions,
includeUndefinedProperties,
indentLevel: indentLevel + 1,
indentString,
lineEnding,
visitedObjects: new Set([value, ...visitedObjects]),
})
: null;
if (propertyValueString) {
const quotedPropertyName = propertyNameRequiresQuotes(propertyName)
? quoteString(propertyName, {
doubleQuote,
})
: propertyName,
trimmedPropertyValueString = propertyValueString.substr((indentLevel + 1) * indentString.length);
if (typeof propertyValue === 'function' && trimmedPropertyValueString.startsWith(`${propertyName}()`)) {
entries.push(
`${indentString.repeat(indentLevel + 1)}${quotedPropertyName} ${trimmedPropertyValueString.substr(
propertyName.length,
)}`,
);
} else {
entries.push(`${indentString.repeat(indentLevel + 1)}${quotedPropertyName}: ${trimmedPropertyValueString}`);
}
}
return entries;
}, []);
return value.length
? `${indentString.repeat(indentLevel)}{${lineEnding}${value.join(
`,${lineEnding}`,
)}${lineEnding}${indentString.repeat(indentLevel)}}`
: `${indentString.repeat(indentLevel)}{}`;
case 'string':
return `${indentString.repeat(indentLevel)}${quoteString(value, {
doubleQuote,
})}`;
case 'symbol': {
let key = Symbol.keyFor(value);
if (typeof key === 'string') {
return `${indentString.repeat(indentLevel)}Symbol.for(${quoteString(key, {
doubleQuote,
})})`;
}
key = value.toString().slice(7, -1);
if (key) {
return `${indentString.repeat(indentLevel)}Symbol(${quoteString(key, {
doubleQuote,
})})`;
}
return `${indentString.repeat(indentLevel)}Symbol()`;
}
case 'undefined':
return `${indentString.repeat(indentLevel)}undefined`;
}
}
export function getSource(value: any): string {
if (value && value.__source) {
return value.__source;
}
let source = valueToSource(value);
if (source === 'undefined') {
source = '';
}
if (value) {
try {
value.__source = source;
} catch (ex) {}
}
return source;
}

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@recore/config/tsconfig",
"compilerOptions": {
"experimentalDecorators": true
},
"include": [
"./src/"
]
}