mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-25 06:52:39 +00:00
initial
This commit is contained in:
parent
8d9f72ca3e
commit
625cba310c
16
packages/designer/.editorconfig
Normal file
16
packages/designer/.editorconfig
Normal 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
|
||||||
6
packages/designer/.eslintignore
Normal file
6
packages/designer/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
build/
|
||||||
|
.*
|
||||||
|
~*
|
||||||
|
node_modules
|
||||||
3
packages/designer/.eslintrc
Normal file
3
packages/designer/.eslintrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/@recore/config/.eslintrc"
|
||||||
|
}
|
||||||
40
packages/designer/.gitignore
vendored
Normal file
40
packages/designer/.gitignore
vendored
Normal 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
|
||||||
6
packages/designer/.prettierrc
Normal file
6
packages/designer/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
40
packages/designer/package.json
Normal file
40
packages/designer/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
29
packages/designer/src/builtins/drag-ghost/ghost.less
Normal file
29
packages/designer/src/builtins/drag-ghost/ghost.less
Normal 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;
|
||||||
|
}
|
||||||
105
packages/designer/src/builtins/drag-ghost/ghost.tsx
Normal file
105
packages/designer/src/builtins/drag-ghost/ghost.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
packages/designer/src/builtins/embed-editor.ts
Normal file
59
packages/designer/src/builtins/embed-editor.ts
Normal 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();
|
||||||
22
packages/designer/src/builtins/simulator/auxilary/README.md
Normal file
22
packages/designer/src/builtins/simulator/auxilary/README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
辅助类
|
||||||
|
对齐线
|
||||||
|
插入指示 insertion 竖线 横线 插入块 禁止插入块
|
||||||
|
幽灵替身 ghost
|
||||||
|
聚焦编辑指示
|
||||||
|
|
||||||
|
|
||||||
|
插入指示 insertion 竖线 横线 插入块 禁止插入块
|
||||||
|
|
||||||
|
竖线:红色,绿色
|
||||||
|
横线:红色,绿色
|
||||||
|
插入块:透明绿色,透明红色
|
||||||
|
|
||||||
|
投放指示线
|
||||||
|
|
||||||
|
cover
|
||||||
|
|
||||||
|
轮廓服务
|
||||||
|
悬停指示线 xray mode?
|
||||||
|
选中指示线
|
||||||
|
投放指示线
|
||||||
|
透视线 x-ray
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/designer/src/builtins/simulator/auxilary/droping.ts
Normal file
13
packages/designer/src/builtins/simulator/auxilary/droping.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// outline
|
||||||
|
// insertion
|
||||||
|
/*
|
||||||
|
// 插入指示 insertion 竖线 横线 插入块 禁止插入块
|
||||||
|
|
||||||
|
竖线:红色,绿色
|
||||||
|
横线:红色,绿色
|
||||||
|
插入块:透明绿色,透明红色
|
||||||
|
|
||||||
|
投放指示线
|
||||||
|
|
||||||
|
cover
|
||||||
|
*/
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
packages/designer/src/builtins/simulator/auxilary/edging.tsx
Normal file
62
packages/designer/src/builtins/simulator/auxilary/edging.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './auxiliary';
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
packages/designer/src/builtins/simulator/auxilary/insertion.tsx
Normal file
139
packages/designer/src/builtins/simulator/auxilary/insertion.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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} />;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
packages/designer/src/builtins/simulator/create-simulator.ts
Normal file
80
packages/designer/src/builtins/simulator/create-simulator.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
0
packages/designer/src/builtins/simulator/index.tsx
Normal file
0
packages/designer/src/builtins/simulator/index.tsx
Normal file
0
packages/designer/src/builtins/simulator/screen
Normal file
0
packages/designer/src/builtins/simulator/screen
Normal file
47
packages/designer/src/designer/canvas.less
Normal file
47
packages/designer/src/designer/canvas.less
Normal 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;
|
||||||
|
}
|
||||||
76
packages/designer/src/designer/canvas.tsx
Normal file
76
packages/designer/src/designer/canvas.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/designer/src/designer/designer.ts
Normal file
18
packages/designer/src/designer/designer.ts
Normal 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;
|
||||||
|
//....
|
||||||
|
}
|
||||||
173
packages/designer/src/designer/document/document-context.ts
Normal file
173
packages/designer/src/designer/document/document-context.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
packages/designer/src/designer/document/history.ts
Normal file
0
packages/designer/src/designer/document/history.ts
Normal file
123
packages/designer/src/designer/document/location.ts
Normal file
123
packages/designer/src/designer/document/location.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
733
packages/designer/src/designer/document/master-board.ts
Normal file
733
packages/designer/src/designer/document/master-board.ts
Normal 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);
|
||||||
|
}
|
||||||
478
packages/designer/src/designer/document/node.ts
Normal file
478
packages/designer/src/designer/document/node.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
558
packages/designer/src/designer/document/props.ts
Normal file
558
packages/designer/src/designer/document/props.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
133
packages/designer/src/designer/document/root-node.ts
Normal file
133
packages/designer/src/designer/document/root-node.ts
Normal 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;
|
||||||
|
}
|
||||||
134
packages/designer/src/designer/document/scroller.ts
Normal file
134
packages/designer/src/designer/document/scroller.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
packages/designer/src/designer/document/selection.ts
Normal file
151
packages/designer/src/designer/document/selection.ts
Normal 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);
|
||||||
|
}
|
||||||
65
packages/designer/src/designer/document/stash-space.ts
Normal file
65
packages/designer/src/designer/document/stash-space.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
96
packages/designer/src/designer/document/viewport.ts
Normal file
96
packages/designer/src/designer/document/viewport.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
349
packages/designer/src/designer/dragon.ts
Normal file
349
packages/designer/src/designer/dragon.ts
Normal 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();
|
||||||
114
packages/designer/src/designer/hotkey.ts
Normal file
114
packages/designer/src/designer/hotkey.ts
Normal 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);
|
||||||
0
packages/designer/src/designer/index.ts
Normal file
0
packages/designer/src/designer/index.ts
Normal file
76
packages/designer/src/designer/project.ts
Normal file
76
packages/designer/src/designer/project.ts
Normal 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 {}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
}
|
||||||
159
packages/designer/src/designer/schema.ts
Normal file
159
packages/designer/src/designer/schema.ts
Normal 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;
|
||||||
|
}
|
||||||
0
packages/designer/src/designer/workspace.tsx
Normal file
0
packages/designer/src/designer/workspace.tsx
Normal file
0
packages/designer/src/index.ts
Normal file
0
packages/designer/src/index.ts
Normal file
95
packages/designer/src/utils/clipboard.ts
Normal file
95
packages/designer/src/utils/clipboard.ts
Normal 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();
|
||||||
23
packages/designer/src/utils/clone-deep.ts
Normal file
23
packages/designer/src/utils/clone-deep.ts
Normal 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;
|
||||||
|
}
|
||||||
17
packages/designer/src/utils/create-content.ts
Normal file
17
packages/designer/src/utils/create-content.ts
Normal 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;
|
||||||
|
}
|
||||||
17
packages/designer/src/utils/create-defer.ts
Normal file
17
packages/designer/src/utils/create-defer.ts
Normal 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;
|
||||||
|
}
|
||||||
15
packages/designer/src/utils/cursor.less
Normal file
15
packages/designer/src/utils/cursor.less
Normal 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
|
||||||
|
}
|
||||||
60
packages/designer/src/utils/cursor.ts
Normal file
60
packages/designer/src/utils/cursor.ts
Normal 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();
|
||||||
19
packages/designer/src/utils/dom.ts
Normal file
19
packages/designer/src/utils/dom.ts
Normal 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());
|
||||||
|
}
|
||||||
7
packages/designer/src/utils/get-prototype-of.ts
Normal file
7
packages/designer/src/utils/get-prototype-of.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function getPrototypeOf(target: any) {
|
||||||
|
if (typeof Object.getPrototypeOf !== 'undefined') {
|
||||||
|
return Object.getPrototypeOf(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.__proto__;
|
||||||
|
}
|
||||||
4
packages/designer/src/utils/has-own-property.ts
Normal file
4
packages/designer/src/utils/has-own-property.ts
Normal 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);
|
||||||
|
}
|
||||||
618
packages/designer/src/utils/hotkey.ts
Normal file
618
packages/designer/src/utils/hotkey.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/designer/src/utils/index.ts
Normal file
9
packages/designer/src/utils/index.ts
Normal 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';
|
||||||
3
packages/designer/src/utils/is-css-url.ts
Normal file
3
packages/designer/src/utils/is-css-url.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isCSSUrl(url: string): boolean {
|
||||||
|
return /\.css$/.test(url);
|
||||||
|
}
|
||||||
3
packages/designer/src/utils/is-es-module.ts
Normal file
3
packages/designer/src/utils/is-es-module.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isESModule(obj: any): obj is { [key: string]: any } {
|
||||||
|
return obj && obj.__esModule;
|
||||||
|
}
|
||||||
3
packages/designer/src/utils/is-function.ts
Normal file
3
packages/designer/src/utils/is-function.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isFunction(fn: any): boolean {
|
||||||
|
return typeof fn === 'function';
|
||||||
|
}
|
||||||
3
packages/designer/src/utils/is-object.ts
Normal file
3
packages/designer/src/utils/is-object.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isObject(value: any): value is object {
|
||||||
|
return value !== null && typeof value === 'object';
|
||||||
|
}
|
||||||
9
packages/designer/src/utils/is-plain-object.ts
Normal file
9
packages/designer/src/utils/is-plain-object.ts
Normal 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;
|
||||||
|
}
|
||||||
9
packages/designer/src/utils/is-react.ts
Normal file
9
packages/designer/src/utils/is-react.ts
Normal 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');
|
||||||
|
}
|
||||||
7
packages/designer/src/utils/parse-code.ts
Normal file
7
packages/designer/src/utils/parse-code.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function parseCode(code: string): string {
|
||||||
|
try {
|
||||||
|
return JSON.parse(code);
|
||||||
|
} catch (e) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
173
packages/designer/src/utils/path.ts
Normal file
173
packages/designer/src/utils/path.ts
Normal 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;
|
||||||
|
}
|
||||||
32
packages/designer/src/utils/react.ts
Normal file
32
packages/designer/src/utils/react.ts
Normal 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;
|
||||||
|
}
|
||||||
38
packages/designer/src/utils/script.ts
Normal file
38
packages/designer/src/utils/script.ts
Normal 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();
|
||||||
|
}
|
||||||
8
packages/designer/src/utils/set-prototype-of.ts
Normal file
8
packages/designer/src/utils/set-prototype-of.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/designer/src/utils/shallow-equal.ts
Normal file
27
packages/designer/src/utils/shallow-equal.ts
Normal 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;
|
||||||
|
}
|
||||||
55
packages/designer/src/utils/style-point.ts
Normal file
55
packages/designer/src/utils/style-point.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/designer/src/utils/throttle.ts
Normal file
100
packages/designer/src/utils/throttle.ts
Normal 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;
|
||||||
|
}
|
||||||
11
packages/designer/src/utils/type-check.ts
Normal file
11
packages/designer/src/utils/type-check.ts
Normal 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;
|
||||||
|
}
|
||||||
4
packages/designer/src/utils/unique-id.ts
Normal file
4
packages/designer/src/utils/unique-id.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
let guid = Date.now();
|
||||||
|
export function uniqueId(prefix = '') {
|
||||||
|
return `${prefix}${(guid++).toString(36).toLowerCase()}`;
|
||||||
|
}
|
||||||
232
packages/designer/src/utils/value-to-source.ts
Normal file
232
packages/designer/src/utils/value-to-source.ts
Normal 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;
|
||||||
|
}
|
||||||
9
packages/designer/tsconfig.json
Normal file
9
packages/designer/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./node_modules/@recore/config/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user