forked from openinula/inula
Compare commits
12 Commits
API-2.0-de
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
f1d4e4d987 | |
|
|
72669eef88 | |
|
|
2c2c3926e7 | |
|
|
ecaaacb812 | |
|
|
6688cde7ab | |
|
|
8a7623d281 | |
|
|
0375ed95fc | |
|
|
4a825cec88 | |
|
|
aa4984f997 | |
|
|
78f4bce57c | |
|
|
ebfe1eceb9 | |
|
|
ec34490202 |
|
|
@ -1,8 +0,0 @@
|
||||||
# Changesets
|
|
||||||
|
|
||||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
||||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
||||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
||||||
|
|
||||||
We have a quick list of common questions to get you started engaging with this project in
|
|
||||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
|
||||||
"changelog": "@changesets/cli/changelog",
|
|
||||||
"commit": false,
|
|
||||||
"fixed": [],
|
|
||||||
"linked": [],
|
|
||||||
"access": "restricted",
|
|
||||||
"baseBranch": "master",
|
|
||||||
"updateInternalDependencies": "patch",
|
|
||||||
"ignore": ["create-inula", "openinula", "inula-cli", "inula-dev-tools", "inula-intl", "inula-request", "inula-router", "inula-vite-app", "inula-webpack-app"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
node_modules
|
/node_modules
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
/packages/**/node_modules
|
||||||
/packages/inula-cli/lib
|
/packages/inula-cli/lib
|
||||||
build
|
build
|
||||||
/packages/inula-router/connectRouter
|
/packages/inula-router/connectRouter
|
||||||
/packages/inula-router/router
|
/packages/inula-router/router
|
||||||
dist
|
|
||||||
.history
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
|
||||||
<title>Inula-next</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="main"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "dev",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/standalone": "^7.22.4",
|
|
||||||
"@openinula/next": "workspace:*",
|
|
||||||
"@iandx/easy-css": "^0.10.14",
|
|
||||||
"babel-preset-inula-next": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^4.4.9",
|
|
||||||
"vite-plugin-inula-next": "workspace:*"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
import { render, View } from '@openinula/next';
|
|
||||||
|
|
||||||
let idCounter = 1;
|
|
||||||
|
|
||||||
const adjectives = [
|
|
||||||
'pretty',
|
|
||||||
'large',
|
|
||||||
'big',
|
|
||||||
'small',
|
|
||||||
'tall',
|
|
||||||
'short',
|
|
||||||
'long',
|
|
||||||
'handsome',
|
|
||||||
'plain',
|
|
||||||
'quaint',
|
|
||||||
'clean',
|
|
||||||
'elegant',
|
|
||||||
'easy',
|
|
||||||
'angry',
|
|
||||||
'crazy',
|
|
||||||
'helpful',
|
|
||||||
'mushy',
|
|
||||||
'odd',
|
|
||||||
'unsightly',
|
|
||||||
'adorable',
|
|
||||||
'important',
|
|
||||||
'inexpensive',
|
|
||||||
'cheap',
|
|
||||||
'expensive',
|
|
||||||
'fancy',
|
|
||||||
];
|
|
||||||
const colours = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange'];
|
|
||||||
const nouns = [
|
|
||||||
'table',
|
|
||||||
'chair',
|
|
||||||
'house',
|
|
||||||
'bbq',
|
|
||||||
'desk',
|
|
||||||
'car',
|
|
||||||
'pony',
|
|
||||||
'cookie',
|
|
||||||
'sandwich',
|
|
||||||
'burger',
|
|
||||||
'pizza',
|
|
||||||
'mouse',
|
|
||||||
'keyboard',
|
|
||||||
];
|
|
||||||
|
|
||||||
function _random(max) {
|
|
||||||
return Math.round(Math.random() * 1000) % max;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildData(count) {
|
|
||||||
const data = new Array(count);
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
data[i] = {
|
|
||||||
id: idCounter++,
|
|
||||||
label: `${adjectives[_random(adjectives.length)]} ${colours[_random(colours.length)]} ${nouns[_random(nouns.length)]}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Button({ id, text, fn }) {
|
|
||||||
return (
|
|
||||||
<div class="col-sm-6 smallpad">
|
|
||||||
<button id={id} class="btn btn-primary btn-block" type="button" onClick={fn}>
|
|
||||||
{text}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let data = [];
|
|
||||||
let selected = null;
|
|
||||||
function run() {
|
|
||||||
data = buildData(1000);
|
|
||||||
}
|
|
||||||
function runLots() {
|
|
||||||
data = buildData(10000);
|
|
||||||
}
|
|
||||||
function add() {
|
|
||||||
data.push(...buildData(1000));
|
|
||||||
}
|
|
||||||
function update() {
|
|
||||||
for (let i = 0; i < data.length; i += 10) {
|
|
||||||
data[i].label += ' !!!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function swapRows() {
|
|
||||||
if (data.length > 998) {
|
|
||||||
[data[1], data[998]] = [data[998], data[1]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function clear() {
|
|
||||||
data = [];
|
|
||||||
}
|
|
||||||
function remove(id) {
|
|
||||||
data = data.filter(d => d.id !== id);
|
|
||||||
}
|
|
||||||
function select(id) {
|
|
||||||
selected = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="container">
|
|
||||||
<div class="jumbotron">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h1>Inula-next Keyed</h1>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="row">
|
|
||||||
<Button id="run" text="Create 1,000 rows" fn={run} />
|
|
||||||
<Button id="runlots" text="Create 10,000 rows" fn={runLots} />
|
|
||||||
<Button id="add" text="Append 1,000 rows" fn={add} />
|
|
||||||
<Button id="update" text="Update every 10th row" fn={update} />
|
|
||||||
<Button id="clear" text="Clear" fn={clear} />
|
|
||||||
<Button id="swaprows" text="Swap Rows" fn={swapRows} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="table table-hover table-striped test-data">
|
|
||||||
<tbody>
|
|
||||||
<for array={data} item={{ id, label }} key={id}>
|
|
||||||
<tr class={selected === id ? 'danger' : ''}>
|
|
||||||
<td class="col-md-1" textContent={id} />
|
|
||||||
<td class="col-md-4">
|
|
||||||
<a onClick={select.bind(this, id)} textContent={label} />
|
|
||||||
</td>
|
|
||||||
<td class="col-md-1">
|
|
||||||
<a onClick={remove.bind(this, id)}>
|
|
||||||
<span class="glyphicon glyphicon-remove" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="col-md-6" />
|
|
||||||
</tr>
|
|
||||||
</for>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, 'main');
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import inula from 'vite-plugin-inula-next';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
server: {
|
|
||||||
port: 4320,
|
|
||||||
},
|
|
||||||
base: '',
|
|
||||||
optimizeDeps: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
plugins: [inula({ files: '**/*.{tsx,jsx}' })],
|
|
||||||
});
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# dev
|
|
||||||
|
|
||||||
## 0.0.1
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [2f9d373]
|
|
||||||
- babel-preset-inula-next@0.0.2
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8"/>
|
|
||||||
<title>Dlight.JS</title>
|
|
||||||
<link rel="stylesheet" href="/src/App.css"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="main"></div>
|
|
||||||
<script type="module" src="/src/App.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "dev",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/standalone": "^7.22.4",
|
|
||||||
"@openinula/next": "workspace:*",
|
|
||||||
"@iandx/easy-css": "^0.10.14",
|
|
||||||
"babel-preset-inula-next": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^4.4.9",
|
|
||||||
"vite-plugin-inula-next": "workspace:*"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.ok {
|
|
||||||
color: var(--color-ok);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { View, render } from '@openinula/next';
|
|
||||||
|
|
||||||
function MyComp() {
|
|
||||||
let count = 0;
|
|
||||||
const db = count * 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className="123">Hello dlight fn comp</h1>
|
|
||||||
<section>
|
|
||||||
count: {count}, double is: {db}
|
|
||||||
<button onClick={() => (count += 1)}>Add</button>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
render(MyComp, 'main');
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "preserve",
|
|
||||||
"target": "ESNext",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"experimentalDecorators": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import inula from 'vite-plugin-inula-next';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
server: {
|
|
||||||
port: 4320,
|
|
||||||
},
|
|
||||||
base: '',
|
|
||||||
optimizeDeps: {
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
plugins: [inula({ files: '**/*.{ts,js,tsx,jsx}' })],
|
|
||||||
});
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
# Todo-list
|
|
||||||
|
|
||||||
- [ ] function 2 class.
|
|
||||||
- [x] assignment 2 property
|
|
||||||
- [x] statement 2 watch func
|
|
||||||
- [ ] handle `props` @HQ
|
|
||||||
- [x] object destructuring
|
|
||||||
- [x] default value
|
|
||||||
- [ ] partial object destructuring
|
|
||||||
- [ ] nested object destructuring
|
|
||||||
- [ ] nested array destructuring
|
|
||||||
- [x] alias
|
|
||||||
- [x] add `this` @HQ
|
|
||||||
- [x] for (jsx-parser) -> playground + benchmark @YH
|
|
||||||
- [x] lifecycle @HQ
|
|
||||||
- [x] ref @HQ (to validate)
|
|
||||||
- [x] env @HQ (to validate)
|
|
||||||
- [ ] Sub component
|
|
||||||
- [ ] Early Return
|
|
||||||
- [ ] custom hook -> Model @YH
|
|
||||||
- [ ] JSX
|
|
||||||
- [x] style
|
|
||||||
- [x] fragment
|
|
||||||
- [x] ref (to validate)
|
|
||||||
- [ ] snippet
|
|
||||||
- [x] for
|
|
||||||
|
|
||||||
|
|
||||||
# function component syntax
|
|
||||||
|
|
||||||
- [ ] props (destructuring | partial destructuring | default value | alias)
|
|
||||||
- [ ] variable declaration -> class component property
|
|
||||||
- [ ] function declaration ( arrow function | async function )-> class method
|
|
||||||
- [ ] Statement -> watch function
|
|
||||||
- [ ] assignment
|
|
||||||
- [ ] function call
|
|
||||||
- [ ] class method call
|
|
||||||
- [ ] for loop
|
|
||||||
- [ ] while loop (do while, while, for, for in, for of)
|
|
||||||
- [ ] if statement
|
|
||||||
- [ ] switch statement
|
|
||||||
- [ ] try catch statement
|
|
||||||
- [ ] throw statement ? not support
|
|
||||||
- [ ] delete expression
|
|
||||||
- [ ] lifecycle -> LabeledStatement
|
|
||||||
- [ ] return statement -> render method(Body)
|
|
||||||
- [ ] iife
|
|
||||||
- [ ] early return
|
|
||||||
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
- [ ] partial props destructuring -> support this.$props @YH
|
|
||||||
```jsx
|
|
||||||
function Input({onClick, xxx, ...props}) {
|
|
||||||
function handleClick() {
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
return <input onClick={handleClick} {...props} />
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [ ] model class declaration should before class component declaration -> use Class polyfill
|
|
||||||
```jsx
|
|
||||||
// Code like this will cause error: `FetchModel` is not defined
|
|
||||||
@Main
|
|
||||||
@View
|
|
||||||
class MyComp {
|
|
||||||
fetchModel = use(FetchModel, { url: "https://api.example.com/data" })
|
|
||||||
|
|
||||||
Body() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Model
|
|
||||||
class FetchModel {}
|
|
||||||
```
|
|
||||||
- [ ] custom hook early return @YH
|
|
||||||
- [ ] snippet
|
|
||||||
```jsx
|
|
||||||
const H1 = <h1></h1>;
|
|
||||||
// {H1}
|
|
||||||
const H1 = (name) => <h1 className={name}></h1>;
|
|
||||||
// {H1()} <H1/>
|
|
||||||
function H1() {
|
|
||||||
return <h1></h1>;
|
|
||||||
}
|
|
||||||
// <H1/>
|
|
||||||
```
|
|
||||||
- [ ] Render text and variable, Got Error
|
|
||||||
```jsx
|
|
||||||
// Uncaught DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
|
|
||||||
<button>Add, Now is {count}</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
# Proposal
|
|
||||||
## Watch
|
|
||||||
|
|
||||||
自动将Statement包裹Watch的反例:
|
|
||||||
```jsx
|
|
||||||
// 前置操作: 场景为Table组件,需要响应column变化,先置空column,再计算新的columnByKey
|
|
||||||
let columnByKey;
|
|
||||||
watch: {
|
|
||||||
columnByKey = {};
|
|
||||||
columns.forEach(col => {
|
|
||||||
columnByKey[col.key] = col;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 临时变量: 场景为操作前的计算部分临时变量
|
|
||||||
watch: {
|
|
||||||
let col = columnByKey[sortBy];
|
|
||||||
if (
|
|
||||||
col !== undefined &&
|
|
||||||
col.sortable === true &&
|
|
||||||
typeof col.value === "function"
|
|
||||||
) {
|
|
||||||
sortFunction = r => col.value(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
"build:inula-intl": "pnpm -F inula-intl build",
|
"build:inula-intl": "pnpm -F inula-intl build",
|
||||||
"build:inula-request": "pnpm -F inula-request build",
|
"build:inula-request": "pnpm -F inula-request build",
|
||||||
"build:inula-router": "pnpm -F inula-router build",
|
"build:inula-router": "pnpm -F inula-router build",
|
||||||
"build:transpiler": "pnpm --filter './packages/transpiler/*' run build",
|
|
||||||
"commitlint": "commitlint --config commitlint.config.js -e",
|
"commitlint": "commitlint --config commitlint.config.js -e",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
|
|
@ -81,9 +80,5 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.x",
|
"node": ">=10.x",
|
||||||
"npm": ">=7.x"
|
"npm": ">=7.x"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@changesets/cli": "^2.27.1",
|
|
||||||
"changeset": "^0.2.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ inula-cli的推荐目录结构如下:
|
||||||
│ └── inula-cli
|
│ └── inula-cli
|
||||||
│ ├── lib
|
│ ├── lib
|
||||||
├── mock // mock目录
|
├── mock // mock目录
|
||||||
│ └── transform.ts
|
│ └── mock.ts
|
||||||
├── src // 项目源码目录
|
├── src // 项目源码目录
|
||||||
│ ├── pages
|
│ ├── pages
|
||||||
│ │ ├── index.less
|
│ │ ├── index.less
|
||||||
|
|
@ -178,10 +178,10 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用
|
||||||
|
|
||||||
inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需安装并运行这些插件。
|
inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需安装并运行这些插件。
|
||||||
|
|
||||||
安装可以通过npm安装,这里以插件@openinula/add为例:
|
安装可以通过npm安装,这里以插件@inula/add为例:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm i --save-dev @openinula/add
|
npm i --save-dev @inula/add
|
||||||
```
|
```
|
||||||
|
|
||||||
如果需要运行插件,需要在配置文件中配置对应的插件路径
|
如果需要运行插件,需要在配置文件中配置对应的插件路径
|
||||||
|
|
@ -191,7 +191,7 @@ npm i --save-dev @openinula/add
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...
|
...
|
||||||
plugins:["@openinula/add"]
|
plugins:["@inula/add"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare module 'crequire';
|
declare module 'crequire';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export default (api: API) => {
|
||||||
api.applyHook({ name: 'afterStartDevServer' });
|
api.applyHook({ name: 'afterStartDevServer' });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
api.logger.error("Can't find config");
|
api.logger.error('Can\'t find config');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'vite':
|
case 'vite':
|
||||||
|
|
@ -70,7 +70,7 @@ export default (api: API) => {
|
||||||
server.printUrls();
|
server.printUrls();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
api.logger.error("Can't find config");
|
api.logger.error('Can\'t find config');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default (api: API) => {
|
||||||
args._.shift();
|
args._.shift();
|
||||||
}
|
}
|
||||||
if (args._.length === 0) {
|
if (args._.length === 0) {
|
||||||
api.logger.warn("Can't find any generate options.");
|
api.logger.warn('Can\'t find any generate options.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@
|
||||||
const { preset } = require('./jest.config');
|
const { preset } = require('./jest.config');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{ targets: { node: 'current' } },
|
||||||
|
],
|
||||||
['@babel/preset-typescript'],
|
['@babel/preset-typescript'],
|
||||||
[
|
[
|
||||||
'@babel/preset-react',
|
'@babel/preset-react',
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default {
|
||||||
{
|
{
|
||||||
file: path.resolve(output, 'intl.esm-browser.js'),
|
file: path.resolve(output, 'intl.esm-browser.js'),
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
doubleapos: { match: "''", value: () => "'" },
|
doubleapos: { match: '\'\'', value: () => '\'' },
|
||||||
quoted: {
|
quoted: {
|
||||||
lineBreaks: true,
|
lineBreaks: true,
|
||||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
||||||
value: src => src.slice(1, -1).replace(/''/g, "'"),
|
value: src => src.slice(1, -1).replace(/''/g, '\''),
|
||||||
},
|
},
|
||||||
argument: {
|
argument: {
|
||||||
lineBreaks: true,
|
lineBreaks: true,
|
||||||
|
|
|
||||||
|
|
@ -90,19 +90,19 @@ describe('I18n', () => {
|
||||||
});
|
});
|
||||||
it('._ allow escaping syntax characters', () => {
|
it('._ allow escaping syntax characters', () => {
|
||||||
const messages = {
|
const messages = {
|
||||||
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
|
'My \'\'name\'\' is \'{name}\'': 'Mi \'\'nombre\'\' es \'{name}\'',
|
||||||
};
|
};
|
||||||
const i18n = new I18n({
|
const i18n = new I18n({
|
||||||
locale: 'es',
|
locale: 'es',
|
||||||
messages: { es: messages },
|
messages: { es: messages },
|
||||||
});
|
});
|
||||||
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
|
expect(i18n.formatMessage('My \'\'name\'\' is \'{name}\'')).toEqual('Mi \'nombre\' es {name}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('._ should format message from catalog', function () {
|
it('._ should format message from catalog', function () {
|
||||||
const messages = {
|
const messages = {
|
||||||
Hello: 'Salut',
|
Hello: 'Salut',
|
||||||
id: "Je m'appelle {name}",
|
id: 'Je m\'appelle {name}',
|
||||||
};
|
};
|
||||||
const i18n = new I18n({
|
const i18n = new I18n({
|
||||||
locale: 'fr',
|
locale: 'fr',
|
||||||
|
|
@ -110,7 +110,7 @@ describe('I18n', () => {
|
||||||
});
|
});
|
||||||
expect(i18n.locale).toEqual('fr');
|
expect(i18n.locale).toEqual('fr');
|
||||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
||||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
|
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual('Je m\'appelle Fred');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the formatted date and time', () => {
|
it('should return the formatted date and time', () => {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ describe('eventEmitter', () => {
|
||||||
expect(listener).not.toBeCalled();
|
expect(listener).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should do nothing when even doesn't exist", () => {
|
it('should do nothing when even doesn\'t exist', () => {
|
||||||
const unknown = jest.fn();
|
const unknown = jest.fn();
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@openinula/store",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "DLight shared store",
|
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"tsup": "^6.5.0",
|
|
||||||
"typescript": "^5.3.2"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"dts": true,
|
|
||||||
"minify": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const Store = {};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# DLight Main Package
|
|
||||||
See the website's documentations for usage.
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@openinula/next",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"inula"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/",
|
|
||||||
"test": "vitest --ui"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"csstype": "^3.1.3",
|
|
||||||
"@openinula/store": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"tsup": "^6.5.0",
|
|
||||||
"vite-plugin-inula-next": "workspace:*",
|
|
||||||
"vitest": "^1.2.2"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.js"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"cjs",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"minify": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,373 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { forwardHTMLProp } from './HTMLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
import { schedule } from './scheduler';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class
|
|
||||||
* @extends import('./DLNode').DLNode
|
|
||||||
*/
|
|
||||||
export class CompNode extends DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, Comp type
|
|
||||||
* @internal
|
|
||||||
* * key - private property key
|
|
||||||
* * $$key - dependency number, e.g. 0b1, 0b10, 0b100
|
|
||||||
* * $s$key - set of properties that depend on this property
|
|
||||||
* * $p$key - exist if this property is a prop
|
|
||||||
* * $e$key - exist if this property is an env
|
|
||||||
* * $en$key - exist if this property is an env, and it's the innermost env that contains this env
|
|
||||||
* * $w$key - exist if this property is a watcher
|
|
||||||
* * $f$key - a function that returns the value of this property, called when the property's dependencies change
|
|
||||||
* * _$children - children nodes of type PropView
|
|
||||||
* * _$contentKey - the key key of the content prop
|
|
||||||
* * _$forwardProps - exist if this node is forwarding props
|
|
||||||
* * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
|
|
||||||
* * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super(DLNodeType.Comp);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Init function, called explicitly in the subclass's constructor
|
|
||||||
* @param props - Object containing properties
|
|
||||||
* @param content - Content to be used
|
|
||||||
* @param children - Child nodes
|
|
||||||
* @param forwardPropsScope - Scope for forwarding properties
|
|
||||||
*/
|
|
||||||
_$init(props, content, children, forwardPropsScope) {
|
|
||||||
this._$notInitd = true;
|
|
||||||
|
|
||||||
// ---- Forward props first to allow internal props to override forwarded props
|
|
||||||
if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
|
|
||||||
if (content) this._$setContent(() => content[0], content[1]);
|
|
||||||
if (props)
|
|
||||||
props.forEach(([key, value, deps]) => {
|
|
||||||
if (key === 'props') return this._$setProps(() => value, deps);
|
|
||||||
this._$setProp(key, () => value, deps);
|
|
||||||
});
|
|
||||||
if (children) this._$children = children;
|
|
||||||
|
|
||||||
// ---- Add envs
|
|
||||||
DLStore.global.DLEnvStore &&
|
|
||||||
Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
|
|
||||||
if (key === '_$catchable') {
|
|
||||||
this._$catchable = value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(`$e$${key}` in this)) return;
|
|
||||||
envNode.addNode(this);
|
|
||||||
this._$initEnv(key, value, envNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
const willCall = () => {
|
|
||||||
this._$callUpdatesBeforeInit();
|
|
||||||
this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
|
|
||||||
this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
|
|
||||||
DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
|
|
||||||
this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
|
|
||||||
this.willMount?.();
|
|
||||||
this._$nodes = this.Body?.() ?? [];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this._$catchable) {
|
|
||||||
this._$catchable(willCall)();
|
|
||||||
if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
|
|
||||||
this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
|
|
||||||
delete this._$catchable;
|
|
||||||
} else {
|
|
||||||
willCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_$setUnmounted() {
|
|
||||||
this._$unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Call updates manually before the node is mounted
|
|
||||||
*/
|
|
||||||
_$callUpdatesBeforeInit() {
|
|
||||||
const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
|
||||||
const ownProps = Object.getOwnPropertyNames(this);
|
|
||||||
const allProps = [...protoProps, ...ownProps];
|
|
||||||
allProps.forEach(key => {
|
|
||||||
// ---- Run watcher
|
|
||||||
if (key.startsWith('$w$')) return this[key.slice(3)]();
|
|
||||||
// ---- Run model update
|
|
||||||
if (key.startsWith('$md$')) {
|
|
||||||
const realKey = key.slice(4);
|
|
||||||
this[realKey] = this[realKey]();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Run derived value
|
|
||||||
if (key.startsWith('$f$')) {
|
|
||||||
const realKey = key.slice(3);
|
|
||||||
this[realKey] = this[key];
|
|
||||||
this._$updateDerived(realKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
delete this._$notInitd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set all the props to forward
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param deps
|
|
||||||
*/
|
|
||||||
_$setPropToForward(key, value, deps) {
|
|
||||||
this._$forwardPropsSet.forEach(node => {
|
|
||||||
const isContent = key === '_$content';
|
|
||||||
if (node._$dlNodeType === DLNodeType.Comp) {
|
|
||||||
if (isContent) node._$setContent(() => value, deps);
|
|
||||||
else node._$setProp(key, () => value, deps);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node instanceof HTMLElement) {
|
|
||||||
if (isContent) key = 'textContent';
|
|
||||||
forwardHTMLProp(node, key, () => value, deps);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Define forward props
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
_$setForwardProp(key, valueFunc, deps) {
|
|
||||||
const notInitd = '_$notInitd' in this;
|
|
||||||
if (!notInitd && this._$cache(key, deps)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
if (key === '_$content' && this._$contentKey) {
|
|
||||||
this[this._$contentKey] = value;
|
|
||||||
this._$updateDerived(this._$contentKey);
|
|
||||||
}
|
|
||||||
this[key] = value;
|
|
||||||
this._$updateDerived(key);
|
|
||||||
if (notInitd) this._$forwardPropsId.push(key);
|
|
||||||
else this._$setPropToForward(key, value, deps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to the set of nodes that are forwarding props to this node and init these props
|
|
||||||
* @param node
|
|
||||||
*/
|
|
||||||
_$addForwardProps(node) {
|
|
||||||
this._$forwardPropsSet.add(node);
|
|
||||||
this._$forwardPropsId.forEach(key => {
|
|
||||||
this._$setPropToForward(key, this[key], []);
|
|
||||||
});
|
|
||||||
DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Cache the deps and return true if the deps are the same as the previous deps
|
|
||||||
* @param key
|
|
||||||
* @param deps
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
_$cache(key, deps) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
const cacheKey = `$cc$${key}`;
|
|
||||||
if (cached(deps, this[cacheKey])) return true;
|
|
||||||
this[cacheKey] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the content prop, the key is stored in _$contentKey
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
_$setContent(valueFunc, deps) {
|
|
||||||
if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
|
|
||||||
const contentKey = this._$contentKey;
|
|
||||||
if (!contentKey) return;
|
|
||||||
if (this._$cache(contentKey, deps)) return;
|
|
||||||
this[contentKey] = valueFunc();
|
|
||||||
this._$updateDerived(contentKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param deps
|
|
||||||
*/
|
|
||||||
_$setProp(key, valueFunc, deps) {
|
|
||||||
if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
|
|
||||||
if (!(`$p$${key}` in this)) {
|
|
||||||
console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._$cache(key, deps)) return;
|
|
||||||
this[key] = valueFunc();
|
|
||||||
this._$updateDerived(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
_$setProps(valueFunc, deps) {
|
|
||||||
if (this._$cache('props', deps)) return;
|
|
||||||
const props = valueFunc();
|
|
||||||
if (!props) return;
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
this._$setProp(key, () => value, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Init an env, put the corresponding innermost envNode in $en$key
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param envNode
|
|
||||||
*/
|
|
||||||
_$initEnv(key, value, envNode) {
|
|
||||||
this[key] = value;
|
|
||||||
this[`$en$${key}`] = envNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Update functions
|
|
||||||
/**
|
|
||||||
* @brief Update an env, called in EnvNode._$update
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
* @param envNode
|
|
||||||
*/
|
|
||||||
_$updateEnv(key, value, envNode) {
|
|
||||||
if (!(`$e$${key}` in this)) return;
|
|
||||||
if (envNode !== this[`$en$${key}`]) return;
|
|
||||||
this[key] = value;
|
|
||||||
this._$updateDerived(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update a prop
|
|
||||||
*/
|
|
||||||
_$ud(exp, key) {
|
|
||||||
this._$updateDerived(key);
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update properties that depend on this property
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
_$updateDerived(key) {
|
|
||||||
if ('_$notInitd' in this) return;
|
|
||||||
|
|
||||||
this[`$s$${key}`]?.forEach(k => {
|
|
||||||
if (`$w$${k}` in this) {
|
|
||||||
// ---- Watcher
|
|
||||||
this[k](key);
|
|
||||||
} else if (`$md$${k}` in this) {
|
|
||||||
this[k]._$update();
|
|
||||||
} else {
|
|
||||||
// ---- Regular derived value
|
|
||||||
this[k] = this[`$f$${k}`];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- "trigger-view"
|
|
||||||
this._$updateView(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
_$updateView(key) {
|
|
||||||
if (this._$modelCallee) return this._$updateModelCallee();
|
|
||||||
if (!('_$update' in this)) return;
|
|
||||||
const depNum = this[`$$${key}`];
|
|
||||||
if (!depNum) return;
|
|
||||||
// ---- Collect all depNums that need to be updated
|
|
||||||
if ('_$depNumsToUpdate' in this) {
|
|
||||||
this._$depNumsToUpdate.push(depNum);
|
|
||||||
} else {
|
|
||||||
this._$depNumsToUpdate = [depNum];
|
|
||||||
// ---- Update in the next microtask
|
|
||||||
schedule(() => {
|
|
||||||
// ---- Abort if unmounted
|
|
||||||
if (this._$unmounted) return;
|
|
||||||
const depNums = this._$depNumsToUpdate;
|
|
||||||
if (depNums.length > 0) {
|
|
||||||
const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
|
|
||||||
this._$update(depNum);
|
|
||||||
}
|
|
||||||
delete this._$depNumsToUpdate;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_$updateModelCallee() {
|
|
||||||
if ('_$depNumsToUpdate' in this) return;
|
|
||||||
this._$depNumsToUpdate = true;
|
|
||||||
// ---- Update in the next microtask
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
// ---- Abort if unmounted
|
|
||||||
if (this._$unmounted) return;
|
|
||||||
this._$modelCallee._$updateDerived(this._$modelKey);
|
|
||||||
delete this._$depNumsToUpdate;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update all props and content of the model
|
|
||||||
*/
|
|
||||||
static _$updateModel(model, propsFunc, contentFunc) {
|
|
||||||
// ---- Suppress update because top level update will be performed
|
|
||||||
// directly by the state variable in the model callee, which will
|
|
||||||
// trigger the update of the model
|
|
||||||
const props = propsFunc() ?? {};
|
|
||||||
const collectedProps = props.s ?? [];
|
|
||||||
props.m?.forEach(([props, deps]) => {
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
collectedProps.push([key, value, deps]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
collectedProps.forEach(([key, value, deps]) => {
|
|
||||||
model._$setProp(key, () => value, deps);
|
|
||||||
});
|
|
||||||
const content = contentFunc();
|
|
||||||
if (content) model._$setContent(() => content[0], content[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static _$releaseModel() {
|
|
||||||
delete this._$modelCallee;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Inject Dlight model in to a property
|
|
||||||
* @param ModelCls
|
|
||||||
* @param props { m: [props, deps], s: [key, value, deps] }
|
|
||||||
* @param content
|
|
||||||
* @param key
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
_$injectModel(ModelCls, propsFunc, contentFunc, key) {
|
|
||||||
const props = propsFunc() ?? {};
|
|
||||||
const collectedProps = props.s ?? [];
|
|
||||||
props.m?.forEach(([props, deps]) => {
|
|
||||||
Object.entries(props).forEach(([key, value]) => {
|
|
||||||
collectedProps.push([key, value, deps]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const model = new ModelCls();
|
|
||||||
model._$init(collectedProps, contentFunc(), null, null);
|
|
||||||
model._$modelCallee = this;
|
|
||||||
model._$modelKey = key;
|
|
||||||
model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- @View -> class Comp extends View
|
|
||||||
export const View = CompNode;
|
|
||||||
export const Model = CompNode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all update functions given the key
|
|
||||||
* @param dlNode
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
export function update(dlNode, key) {
|
|
||||||
dlNode._$updateDerived(key);
|
|
||||||
}
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
import { DLStore } from './store';
|
|
||||||
|
|
||||||
export const DLNodeType = {
|
|
||||||
Comp: 0,
|
|
||||||
For: 1,
|
|
||||||
Cond: 2,
|
|
||||||
Env: 3,
|
|
||||||
Exp: 4,
|
|
||||||
Snippet: 5,
|
|
||||||
Try: 6,
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Node type: HTML, Text, Custom, For, If, Env, Expression
|
|
||||||
*/
|
|
||||||
_$dlNodeType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor
|
|
||||||
* @param nodeType
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
constructor(nodeType) {
|
|
||||||
this._$dlNodeType = nodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Node element
|
|
||||||
* Either one real element for HTMLNode and TextNode
|
|
||||||
* Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
|
|
||||||
*/
|
|
||||||
get _$el() {
|
|
||||||
return DLNode.toEls(this._$nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Loop all child DLNodes to get all the child elements
|
|
||||||
* @param nodes
|
|
||||||
* @returns HTMLElement[]
|
|
||||||
*/
|
|
||||||
static toEls(nodes) {
|
|
||||||
const els = [];
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
els.push(el);
|
|
||||||
});
|
|
||||||
return els;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Loop nodes ----
|
|
||||||
/**
|
|
||||||
* @brief Loop all elements shallowly,
|
|
||||||
* i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
|
|
||||||
* @param nodes
|
|
||||||
* @param runFunc
|
|
||||||
*/
|
|
||||||
static loopShallowEls(nodes, runFunc) {
|
|
||||||
const stack = [...nodes].reverse();
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop();
|
|
||||||
if (!('_$dlNodeType' in node)) runFunc(node);
|
|
||||||
else node._$nodes && stack.push(...[...node._$nodes].reverse());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add parentEl to all nodes until the first element
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
*/
|
|
||||||
static addParentEl(nodes, parentEl) {
|
|
||||||
nodes.forEach(node => {
|
|
||||||
if ('_$dlNodeType' in node) {
|
|
||||||
node._$parentEl = parentEl;
|
|
||||||
node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Flow index and add child elements ----
|
|
||||||
/**
|
|
||||||
* @brief Get the total count of dom elements before the stop node
|
|
||||||
* @param nodes
|
|
||||||
* @param stopNode
|
|
||||||
* @returns total count of dom elements
|
|
||||||
*/
|
|
||||||
static getFlowIndexFromNodes(nodes, stopNode) {
|
|
||||||
let index = 0;
|
|
||||||
const stack = [...nodes].reverse();
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const node = stack.pop();
|
|
||||||
if (node === stopNode) break;
|
|
||||||
if ('_$dlNodeType' in node) {
|
|
||||||
node._$nodes && stack.push(...[...node._$nodes].reverse());
|
|
||||||
} else {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Given an array of nodes, append them to the parentEl
|
|
||||||
* 1. If nextSibling is provided, insert the nodes before the nextSibling
|
|
||||||
* 2. If nextSibling is not provided, append the nodes to the parentEl
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param nextSibling
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodesWithSibling(nodes, parentEl, nextSibling) {
|
|
||||||
if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
|
|
||||||
return this.appendNodes(nodes, parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Given an array of nodes, append them to the parentEl using the index
|
|
||||||
* 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
|
|
||||||
* 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param index
|
|
||||||
* @param length
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodesWithIndex(nodes, parentEl, index, length) {
|
|
||||||
length = length ?? parentEl.childNodes.length;
|
|
||||||
if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
|
|
||||||
return this.appendNodes(nodes, parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Insert nodes before the nextSibling
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @param nextSibling
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static insertNodesBefore(nodes, parentEl, nextSibling) {
|
|
||||||
let count = 0;
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
parentEl.insertBefore(el, nextSibling);
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Append nodes to the parentEl
|
|
||||||
* @param nodes
|
|
||||||
* @param parentEl
|
|
||||||
* @returns Added element count
|
|
||||||
*/
|
|
||||||
static appendNodes(nodes, parentEl) {
|
|
||||||
let count = 0;
|
|
||||||
this.loopShallowEls(nodes, el => {
|
|
||||||
parentEl.appendChild(el);
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Lifecycle ----
|
|
||||||
/**
|
|
||||||
* @brief Add willUnmount function to node
|
|
||||||
* @param node
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addWillUnmount(node, func) {
|
|
||||||
const willUnmountStore = DLStore.global.WillUnmountStore;
|
|
||||||
const currentStore = willUnmountStore[willUnmountStore.length - 1];
|
|
||||||
// ---- If the current store is empty, it means this node is not mutable
|
|
||||||
if (!currentStore) return;
|
|
||||||
currentStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add didUnmount function to node
|
|
||||||
* @param node
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addDidUnmount(node, func) {
|
|
||||||
const didUnmountStore = DLStore.global.DidUnmountStore;
|
|
||||||
const currentStore = didUnmountStore[didUnmountStore.length - 1];
|
|
||||||
// ---- If the current store is empty, it means this node is not mutable
|
|
||||||
if (!currentStore) return;
|
|
||||||
currentStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add didUnmount function to global store
|
|
||||||
* @param func
|
|
||||||
*/
|
|
||||||
static addDidMount(node, func) {
|
|
||||||
if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
|
|
||||||
DLStore.global.DidMountStore.push(func.bind(null, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all didMount functions and reset the global store
|
|
||||||
*/
|
|
||||||
static runDidMount() {
|
|
||||||
const didMountStore = DLStore.global.DidMountStore;
|
|
||||||
if (!didMountStore || didMountStore.length === 0) return;
|
|
||||||
for (let i = didMountStore.length - 1; i >= 0; i--) {
|
|
||||||
didMountStore[i]();
|
|
||||||
}
|
|
||||||
DLStore.global.DidMountStore = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
export class EnvStoreClass {
|
|
||||||
constructor() {
|
|
||||||
this.envs = {};
|
|
||||||
this.currentEnvNodes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to the current env and merge envs
|
|
||||||
* @param node - The node to add
|
|
||||||
*/
|
|
||||||
addEnvNode(node) {
|
|
||||||
this.currentEnvNodes.push(node);
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Replace the current env with the given nodes and merge envs
|
|
||||||
* @param nodes - The nodes to replace the current environment with
|
|
||||||
*/
|
|
||||||
replaceEnvNodes(nodes) {
|
|
||||||
this.currentEnvNodes = nodes;
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove the last node from the current env and merge envs
|
|
||||||
*/
|
|
||||||
removeEnvNode() {
|
|
||||||
this.currentEnvNodes.pop();
|
|
||||||
this.mergeEnvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
|
|
||||||
*/
|
|
||||||
mergeEnvs() {
|
|
||||||
this.envs = {};
|
|
||||||
this.currentEnvNodes.forEach(envNode => {
|
|
||||||
Object.entries(envNode.envs).forEach(([key, value]) => {
|
|
||||||
this.envs[key] = [value, envNode];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EnvNode extends DLNode {
|
|
||||||
constructor(envs, depsArr) {
|
|
||||||
super(DLNodeType.Env);
|
|
||||||
// Declare a global variable to store the environment variables
|
|
||||||
if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
|
|
||||||
|
|
||||||
this.envs = envs;
|
|
||||||
this.depsArr = depsArr;
|
|
||||||
this.updateNodes = new Set();
|
|
||||||
|
|
||||||
DLStore.global.DLEnvStore.addEnvNode(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
cached(deps, name) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
if (cached(deps, this.depsArr[name])) return true;
|
|
||||||
this.depsArr[name] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update a specific env, and update all the comp nodes that depend on this env
|
|
||||||
* @param name - The name of the environment variable to update
|
|
||||||
* @param value - The new value of the environment variable
|
|
||||||
*/
|
|
||||||
updateEnv(name, valueFunc, deps) {
|
|
||||||
if (this.cached(deps, name)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
this.envs[name] = value;
|
|
||||||
if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
|
|
||||||
DLStore.global.DLEnvStore.mergeEnvs();
|
|
||||||
}
|
|
||||||
this.updateNodes.forEach(node => {
|
|
||||||
node._$updateEnv(name, value, this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
|
|
||||||
* @param node - The node to add
|
|
||||||
*/
|
|
||||||
addNode(node) {
|
|
||||||
this.updateNodes.add(node);
|
|
||||||
DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set this._$nodes, and exit the current env
|
|
||||||
* @param nodes - The nodes to set
|
|
||||||
*/
|
|
||||||
initNodes(nodes) {
|
|
||||||
this._$nodes = nodes;
|
|
||||||
DLStore.global.DLEnvStore.removeEnvNode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
function cache(el, key, deps) {
|
|
||||||
if (deps.length === 0) return false;
|
|
||||||
const cacheKey = `$${key}`;
|
|
||||||
if (cached(deps, el[cacheKey])) return true;
|
|
||||||
el[cacheKey] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set style
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setStyle(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, value]) => {
|
|
||||||
if (key.startsWith('--')) {
|
|
||||||
el.style.setProperty(key, value);
|
|
||||||
} else {
|
|
||||||
el.style[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set dataset
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setDataset(el, value) {
|
|
||||||
Object.assign(el.dataset, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set HTML property with checking value equality first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLProp(el, key, valueFunc, deps) {
|
|
||||||
// ---- Comparing deps, same value won't trigger
|
|
||||||
// will lead to a bug if the value is set outside of the DLNode
|
|
||||||
// e.g. setHTMLProp(el, "textContent", "value", [])
|
|
||||||
// => el.textContent = "other"
|
|
||||||
// => setHTMLProp(el, "textContent", "value", [])
|
|
||||||
// The value will be set to "other" instead of "value"
|
|
||||||
if (cache(el, key, deps)) return;
|
|
||||||
el[key] = valueFunc();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set HTML properties
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLProps(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
if (key === 'style') return setStyle(el, v);
|
|
||||||
if (key === 'dataset') return setDataset(el, v);
|
|
||||||
setHTMLProp(el, key, () => v, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set HTML attribute with checking value equality first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLAttr(el, key, valueFunc, deps) {
|
|
||||||
if (cache(el, key, deps)) return;
|
|
||||||
el.setAttribute(key, valueFunc());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Plainly set HTML attributes
|
|
||||||
* @param el
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setHTMLAttrs(el, value) {
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
setHTMLAttr(el, key, () => v, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function setEvent(el, key, value) {
|
|
||||||
const prevEvent = el[`$on${key}`];
|
|
||||||
if (prevEvent) el.removeEventListener(key, prevEvent);
|
|
||||||
el.addEventListener(key, value);
|
|
||||||
el[`$on${key}`] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventHandler(e) {
|
|
||||||
const key = `$$${e.type}`;
|
|
||||||
for (const node of e.composedPath()) {
|
|
||||||
if (node[key]) node[key](e);
|
|
||||||
if (e.cancelBubble) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function delegateEvent(el, key, value) {
|
|
||||||
if (el[`$$${key}`] === value) return;
|
|
||||||
el[`$$${key}`] = value;
|
|
||||||
if (!DLStore.delegatedEvents.has(key)) {
|
|
||||||
DLStore.delegatedEvents.add(key);
|
|
||||||
DLStore.document.addEventListener(key, eventHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Shortcut for document.createElement
|
|
||||||
* @param tag
|
|
||||||
* @returns HTMLElement
|
|
||||||
*/
|
|
||||||
export function createElement(tag) {
|
|
||||||
return DLStore.document.createElement(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
|
|
||||||
* @param el
|
|
||||||
* @param node
|
|
||||||
* @param position
|
|
||||||
*/
|
|
||||||
export function insertNode(el, node, position) {
|
|
||||||
// ---- Set _$nodes
|
|
||||||
if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
|
|
||||||
el._$nodes.splice(position, 0, node);
|
|
||||||
|
|
||||||
// ---- Insert nodes' elements
|
|
||||||
const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
|
|
||||||
DLNode.appendNodesWithIndex([node], el, flowIdx);
|
|
||||||
// ---- Set parentEl
|
|
||||||
DLNode.addParentEl([node], el);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief An inclusive assign prop function that accepts any type of prop
|
|
||||||
* @param el
|
|
||||||
* @param key
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function forwardHTMLProp(el, key, valueFunc, deps) {
|
|
||||||
if (key === 'style') return setStyle(el, valueFunc());
|
|
||||||
if (key === 'dataset') return setDataset(el, valueFunc());
|
|
||||||
if (key === 'element') return;
|
|
||||||
if (key === 'prop') return setHTMLProps(el, valueFunc());
|
|
||||||
if (key === 'attr') return setHTMLAttrs(el, valueFunc());
|
|
||||||
if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
|
|
||||||
if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
|
|
||||||
if (key === 'forwardProp') return;
|
|
||||||
if (key.startsWith('on')) {
|
|
||||||
return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
|
|
||||||
}
|
|
||||||
setHTMLAttr(el, key, valueFunc, deps);
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
|
|
||||||
export class CondNode extends FlatNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, If type, accept a function that returns a list of nodes
|
|
||||||
* @param caseFunc
|
|
||||||
*/
|
|
||||||
constructor(depNum, condFunc) {
|
|
||||||
super(DLNodeType.Cond);
|
|
||||||
this.depNum = depNum;
|
|
||||||
this.cond = -1;
|
|
||||||
this.condFunc = condFunc;
|
|
||||||
this.initUnmountStore();
|
|
||||||
this._$nodes = this.condFunc(this);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
|
|
||||||
// ---- Add to the global UnmountStore
|
|
||||||
CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
|
||||||
CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes in the environment
|
|
||||||
*/
|
|
||||||
updateCond(key) {
|
|
||||||
// ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
|
|
||||||
// The reason is that if it didn't change, we don't need to unmount or remove the nodes
|
|
||||||
const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
|
||||||
const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
|
|
||||||
|
|
||||||
// ---- If the new nodes are the same as the old nodes, we only need to update children
|
|
||||||
if (this.didntChange) {
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
|
||||||
this.didntChange = false;
|
|
||||||
this.updateFunc?.(this.depNum, key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Remove old nodes
|
|
||||||
const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
|
|
||||||
this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
|
|
||||||
[this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
|
|
||||||
|
|
||||||
if (newNodes.length === 0) {
|
|
||||||
// ---- No branch has been taken
|
|
||||||
this._$nodes = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// ---- Add new nodes
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
// ---- Faster append with nextSibling rather than flowIndex
|
|
||||||
const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
CondNode.runDidMount();
|
|
||||||
this._$nodes = newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The update function of IfNode's childNodes is stored in the first child node
|
|
||||||
* @param changed
|
|
||||||
*/
|
|
||||||
update(changed) {
|
|
||||||
if (!(~this.depNum & changed)) return;
|
|
||||||
this.updateFunc?.(changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
import { DLStore, cached } from '../store';
|
|
||||||
|
|
||||||
export class ExpNode extends FlatNode {
|
|
||||||
/**
|
|
||||||
* @brief Constructor, Exp type, accept a function that returns a list of nodes
|
|
||||||
* @param nodesFunc
|
|
||||||
*/
|
|
||||||
constructor(value, deps) {
|
|
||||||
super(DLNodeType.Exp);
|
|
||||||
this.initUnmountStore();
|
|
||||||
this._$nodes = ExpNode.formatNodes(value);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
this.deps = this.parseDeps(deps);
|
|
||||||
// ---- Add to the global UnmountStore
|
|
||||||
ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
|
|
||||||
ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
parseDeps(deps) {
|
|
||||||
return deps.map(dep => {
|
|
||||||
// ---- CompNode
|
|
||||||
if (dep?.prototype?._$init) return dep.toString();
|
|
||||||
// ---- SnippetNode
|
|
||||||
if (dep?.propViewFunc) return dep.propViewFunc.toString();
|
|
||||||
return dep;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cache(deps) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
deps = this.parseDeps(deps);
|
|
||||||
if (cached(deps, this.deps)) return true;
|
|
||||||
this.deps = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @brief Generate new nodes and replace the old nodes
|
|
||||||
*/
|
|
||||||
update(valueFunc, deps) {
|
|
||||||
if (this.cache(deps)) return;
|
|
||||||
this.removeNodes(this._$nodes);
|
|
||||||
const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
|
|
||||||
if (newNodes.length === 0) {
|
|
||||||
this._$nodes = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Add new nodes
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
ExpNode.runDidMount();
|
|
||||||
|
|
||||||
this._$nodes = newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Format the nodes
|
|
||||||
* @param nodes
|
|
||||||
* @returns New nodes
|
|
||||||
*/
|
|
||||||
static formatNodes(nodes) {
|
|
||||||
if (!Array.isArray(nodes)) nodes = [nodes];
|
|
||||||
return (
|
|
||||||
nodes
|
|
||||||
// ---- Flatten the nodes
|
|
||||||
.flat(1)
|
|
||||||
// ---- Filter out empty nodes
|
|
||||||
.filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
|
|
||||||
.map(node => {
|
|
||||||
// ---- If the node is a string, number or bigint, convert it to a text node
|
|
||||||
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
|
|
||||||
return DLStore.document.createTextNode(`${node}`);
|
|
||||||
}
|
|
||||||
// ---- If the node has PropView, call it to get the view
|
|
||||||
if ('propViewFunc' in node) return node.build();
|
|
||||||
return node;
|
|
||||||
})
|
|
||||||
// ---- Flatten the nodes again
|
|
||||||
.flat(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { DLStore } from '../store';
|
|
||||||
import { MutableNode } from './MutableNode';
|
|
||||||
|
|
||||||
export class FlatNode extends MutableNode {
|
|
||||||
willUnmountFuncs = [];
|
|
||||||
didUnmountFuncs = [];
|
|
||||||
|
|
||||||
setUnmountFuncs() {
|
|
||||||
this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
|
|
||||||
this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
runWillUnmount() {
|
|
||||||
for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
|
|
||||||
}
|
|
||||||
|
|
||||||
runDidUnmount() {
|
|
||||||
for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNodes(nodes) {
|
|
||||||
this.runWillUnmount();
|
|
||||||
super.removeNodes(nodes);
|
|
||||||
this.runDidUnmount();
|
|
||||||
}
|
|
||||||
|
|
||||||
geneNewNodesInEnv(newNodesFunc) {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const nodes = super.geneNewNodesInEnv(newNodesFunc);
|
|
||||||
this.setUnmountFuncs();
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,406 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { DLStore } from '../store';
|
|
||||||
import { MutableNode } from './MutableNode';
|
|
||||||
|
|
||||||
export class ForNode extends MutableNode {
|
|
||||||
array;
|
|
||||||
nodeFunc;
|
|
||||||
depNum;
|
|
||||||
|
|
||||||
nodesMap = new Map();
|
|
||||||
updateArr = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Getter for nodes
|
|
||||||
*/
|
|
||||||
get _$nodes() {
|
|
||||||
const nodes = [];
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor, For type
|
|
||||||
* @param array
|
|
||||||
* @param nodeFunc
|
|
||||||
* @param keys
|
|
||||||
*/
|
|
||||||
constructor(array, depNum, keys, nodeFunc) {
|
|
||||||
super(DLNodeType.For);
|
|
||||||
this.array = [...array];
|
|
||||||
this.keys = keys;
|
|
||||||
this.depNum = depNum;
|
|
||||||
this.addNodeFunc(nodeFunc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief To be called immediately after the constructor
|
|
||||||
* @param nodeFunc
|
|
||||||
*/
|
|
||||||
addNodeFunc(nodeFunc) {
|
|
||||||
this.nodeFunc = nodeFunc;
|
|
||||||
this.array.forEach((item, idx) => {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const key = this.keys?.[idx] ?? idx;
|
|
||||||
const nodes = nodeFunc(item, this.updateArr, idx);
|
|
||||||
this.nodesMap.set(key, nodes);
|
|
||||||
this.setUnmountMap(key);
|
|
||||||
});
|
|
||||||
// ---- For nested ForNode, the whole strategy is just like EnvStore
|
|
||||||
// we use array of function array to create "environment", popping and pushing
|
|
||||||
ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
|
|
||||||
ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the view related to one item in the array
|
|
||||||
* @param nodes
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
updateItem(idx, array, changed) {
|
|
||||||
// ---- The update function of ForNode's childNodes is stored in the first child node
|
|
||||||
this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateItems(changed) {
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
this.updateItem(idx, this.array, changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Non-array update function
|
|
||||||
* @param changed
|
|
||||||
*/
|
|
||||||
update(changed) {
|
|
||||||
// ---- e.g. this.depNum -> 1110 changed-> 1010
|
|
||||||
// ~this.depNum & changed -> ~1110 & 1010 -> 0000
|
|
||||||
// no update because depNum contains all the changed
|
|
||||||
// ---- e.g. this.depNum -> 1110 changed-> 1101
|
|
||||||
// ~this.depNum & changed -> ~1110 & 1101 -> 0001
|
|
||||||
// update because depNum doesn't contain all the changed
|
|
||||||
if (!(~this.depNum & changed)) return;
|
|
||||||
this.updateItems(changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Array-related update function
|
|
||||||
* @param newArray
|
|
||||||
* @param newKeys
|
|
||||||
*/
|
|
||||||
updateArray(newArray, newKeys) {
|
|
||||||
if (newKeys) {
|
|
||||||
this.updateWithKey(newArray, newKeys);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateWithOutKey(newArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shortcut to generate new nodes with idx and key
|
|
||||||
*/
|
|
||||||
getNewNodes(idx, key, array, updateArr) {
|
|
||||||
this.initUnmountStore();
|
|
||||||
const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
|
|
||||||
this.setUnmountMap(key);
|
|
||||||
this.nodesMap.set(key, nodes);
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Set the unmount map by getting the last unmount map from the global store
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
setUnmountMap(key) {
|
|
||||||
const willUnmountMap = DLStore.global.WillUnmountStore.pop();
|
|
||||||
if (willUnmountMap && willUnmountMap.length > 0) {
|
|
||||||
if (!this.willUnmountMap) this.willUnmountMap = new Map();
|
|
||||||
this.willUnmountMap.set(key, willUnmountMap);
|
|
||||||
}
|
|
||||||
const didUnmountMap = DLStore.global.DidUnmountStore.pop();
|
|
||||||
if (didUnmountMap && didUnmountMap.length > 0) {
|
|
||||||
if (!this.didUnmountMap) this.didUnmountMap = new Map();
|
|
||||||
this.didUnmountMap.set(key, didUnmountMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all the unmount functions and clear the unmount map
|
|
||||||
*/
|
|
||||||
runAllWillUnmount() {
|
|
||||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
|
||||||
this.willUnmountMap.forEach(funcs => {
|
|
||||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
|
||||||
});
|
|
||||||
this.willUnmountMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run all the unmount functions and clear the unmount map
|
|
||||||
*/
|
|
||||||
runAllDidUnmount() {
|
|
||||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
|
||||||
this.didUnmountMap.forEach(funcs => {
|
|
||||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
|
||||||
});
|
|
||||||
this.didUnmountMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run the unmount functions of the given key
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
runWillUnmount(key) {
|
|
||||||
if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
|
|
||||||
const funcs = this.willUnmountMap.get(key);
|
|
||||||
if (!funcs) return;
|
|
||||||
for (let i = 0; i < funcs.length; i++) funcs[i]?.();
|
|
||||||
this.willUnmountMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run the unmount functions of the given key
|
|
||||||
*/
|
|
||||||
runDidUnmount(key) {
|
|
||||||
if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
|
|
||||||
const funcs = this.didUnmountMap.get(key);
|
|
||||||
if (!funcs) return;
|
|
||||||
for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
|
|
||||||
this.didUnmountMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
|
||||||
* @param nodes
|
|
||||||
* @param key
|
|
||||||
*/
|
|
||||||
removeNodes(nodes, key) {
|
|
||||||
this.runWillUnmount(key);
|
|
||||||
super.removeNodes(nodes);
|
|
||||||
this.runDidUnmount(key);
|
|
||||||
this.nodesMap.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes without keys
|
|
||||||
* @param newArray
|
|
||||||
*/
|
|
||||||
updateWithOutKey(newArray) {
|
|
||||||
const preLength = this.array.length;
|
|
||||||
const currLength = newArray.length;
|
|
||||||
|
|
||||||
if (preLength === currLength) {
|
|
||||||
// ---- If the length is the same, we only need to update the nodes
|
|
||||||
for (let idx = 0; idx < this.array.length; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
// ---- If the new array is longer, add new nodes directly
|
|
||||||
if (preLength < currLength) {
|
|
||||||
let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
|
||||||
// so we use a length variable to store the length
|
|
||||||
const length = parentEl.childNodes.length;
|
|
||||||
for (let idx = 0; idx < currLength; idx++) {
|
|
||||||
if (idx < preLength) {
|
|
||||||
flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const newNodes = this.getNewNodes(idx, idx, newArray);
|
|
||||||
ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Update the nodes first
|
|
||||||
for (let idx = 0; idx < currLength; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
// ---- If the new array is shorter, remove the extra nodes
|
|
||||||
for (let idx = currLength; idx < preLength; idx++) {
|
|
||||||
const nodes = this.nodesMap.get(idx);
|
|
||||||
this.removeNodes(nodes, idx);
|
|
||||||
}
|
|
||||||
this.updateArr.splice(currLength, preLength - currLength);
|
|
||||||
this.array = [...newArray];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update the nodes with keys
|
|
||||||
* @param newArray
|
|
||||||
* @param newKeys
|
|
||||||
*/
|
|
||||||
updateWithKey(newArray, newKeys) {
|
|
||||||
if (newKeys.length !== new Set(newKeys).size) {
|
|
||||||
throw new Error('DLight: Duplicate keys in for loop are not allowed');
|
|
||||||
}
|
|
||||||
const prevKeys = this.keys;
|
|
||||||
this.keys = newKeys;
|
|
||||||
|
|
||||||
if (ForNode.arrayEqual(prevKeys, this.keys)) {
|
|
||||||
// ---- If the keys are the same, we only need to update the nodes
|
|
||||||
for (let idx = 0; idx < newArray.length; idx++) {
|
|
||||||
this.updateItem(idx, newArray);
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
|
|
||||||
// ---- No nodes after, delete all nodes
|
|
||||||
if (this.keys.length === 0) {
|
|
||||||
const parentNodes = parentEl._$nodes ?? [];
|
|
||||||
if (parentNodes.length === 1 && parentNodes[0] === this) {
|
|
||||||
// ---- ForNode is the only node in the parent node
|
|
||||||
// Frequently used in real life scenarios because we tend to always wrap for with a div element,
|
|
||||||
// so we optimize it here
|
|
||||||
this.runAllWillUnmount();
|
|
||||||
parentEl.innerHTML = '';
|
|
||||||
this.runAllDidUnmount();
|
|
||||||
} else {
|
|
||||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
|
||||||
const prevKey = prevKeys[prevIdx];
|
|
||||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.nodesMap.clear();
|
|
||||||
this.updateArr = [];
|
|
||||||
this.array = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Record how many nodes are before this ForNode with the same parentNode
|
|
||||||
const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
|
|
||||||
// ---- No nodes before, append all nodes
|
|
||||||
if (prevKeys.length === 0) {
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
|
|
||||||
ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
this.array = [...newArray];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shuffleKeys = [];
|
|
||||||
const newUpdateArr = [];
|
|
||||||
|
|
||||||
// ---- 1. Delete the nodes that are no longer in the array
|
|
||||||
for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
|
|
||||||
const prevKey = prevKeys[prevIdx];
|
|
||||||
if (this.keys.includes(prevKey)) {
|
|
||||||
shuffleKeys.push(prevKey);
|
|
||||||
newUpdateArr.push(this.updateArr[prevIdx]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.removeNodes(this.nodesMap.get(prevKey), prevKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 2. Add the nodes that are not in the array but in the new array
|
|
||||||
// ---- Calling parentEl.childNodes.length is time-consuming,
|
|
||||||
// so we use a length variable to store the length
|
|
||||||
let length = parentEl.childNodes.length;
|
|
||||||
let newFlowIndex = flowIndex;
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const key = this.keys[idx];
|
|
||||||
const prevIdx = shuffleKeys.indexOf(key);
|
|
||||||
if (prevIdx !== -1) {
|
|
||||||
// ---- These nodes are already in the parentEl,
|
|
||||||
// and we need to keep track of their flowIndex
|
|
||||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
|
||||||
newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
|
|
||||||
newUpdateArr.splice(idx, 0, null);
|
|
||||||
const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
|
|
||||||
// ---- Add the new nodes
|
|
||||||
shuffleKeys.splice(idx, 0, key);
|
|
||||||
|
|
||||||
const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
|
|
||||||
newFlowIndex += count;
|
|
||||||
length += count;
|
|
||||||
}
|
|
||||||
ForNode.runDidMount();
|
|
||||||
|
|
||||||
// ---- After adding and deleting, the only thing left is to reorder the nodes,
|
|
||||||
// but if the keys are the same, we don't need to reorder
|
|
||||||
if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
|
|
||||||
this.array = [...newArray];
|
|
||||||
this.updateArr = newUpdateArr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newFlowIndex = flowIndex;
|
|
||||||
const bufferNodes = new Map();
|
|
||||||
// ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
|
|
||||||
for (let idx = 0; idx < this.keys.length; idx++) {
|
|
||||||
const key = this.keys[idx];
|
|
||||||
const prevIdx = shuffleKeys.indexOf(key);
|
|
||||||
|
|
||||||
const bufferedNode = bufferNodes.get(key);
|
|
||||||
if (bufferedNode) {
|
|
||||||
// ---- We need to add the flowIndex of the bufferedNode,
|
|
||||||
// because the bufferedNode is in the parentEl and the new position is ahead of the previous position
|
|
||||||
const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
|
|
||||||
const lastEl = ForNode.toEls(bufferedNode).pop();
|
|
||||||
const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
|
|
||||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
|
||||||
// ---- If the node is buffered, we need to add it to the parentEl
|
|
||||||
ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
// ---- So the added length is the length of the bufferedNode
|
|
||||||
newFlowIndex += bufferedFlowIndex;
|
|
||||||
delete bufferNodes[idx];
|
|
||||||
} else if (prevIdx === idx) {
|
|
||||||
// ---- If the node is in the same position, we don't need to do anything
|
|
||||||
newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// ---- If the node is not in the same position, we need to buffer it
|
|
||||||
// We buffer the node of the previous position, and then replace it with the node of the current position
|
|
||||||
const prevKey = shuffleKeys[idx];
|
|
||||||
bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
|
|
||||||
// ---- Length would never change, and the last will always be in the same position,
|
|
||||||
// so it'll always be insertBefore instead of appendChild
|
|
||||||
const childNodes = this.nodesMap.get(key);
|
|
||||||
const lastEl = ForNode.toEls(childNodes).pop();
|
|
||||||
const nextSibling = parentEl.childNodes[newFlowIndex];
|
|
||||||
if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
|
|
||||||
newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ---- Swap the keys
|
|
||||||
const tempKey = shuffleKeys[idx];
|
|
||||||
shuffleKeys[idx] = shuffleKeys[prevIdx];
|
|
||||||
shuffleKeys[prevIdx] = tempKey;
|
|
||||||
const tempUpdateFunc = newUpdateArr[idx];
|
|
||||||
newUpdateArr[idx] = newUpdateArr[prevIdx];
|
|
||||||
newUpdateArr[prevIdx] = tempUpdateFunc;
|
|
||||||
}
|
|
||||||
this.array = [...newArray];
|
|
||||||
this.updateArr = newUpdateArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Compare two arrays
|
|
||||||
* @param arr1
|
|
||||||
* @param arr2
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static arrayEqual(arr1, arr2) {
|
|
||||||
if (arr1.length !== arr2.length) return false;
|
|
||||||
return arr1.every((item, idx) => item === arr2[idx]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { DLNode } from '../DLNode';
|
|
||||||
import { DLStore } from '../store';
|
|
||||||
|
|
||||||
export class MutableNode extends DLNode {
|
|
||||||
/**
|
|
||||||
* @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
|
|
||||||
* 1. The environment of the new nodes should be the same as the old nodes
|
|
||||||
* 2. The new nodes should be added to the parentEl
|
|
||||||
* 3. The old nodes should be removed from the parentEl
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
constructor(type) {
|
|
||||||
super(type);
|
|
||||||
// ---- Save the current environment nodes, must be a new reference
|
|
||||||
if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
|
|
||||||
this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initialize the new nodes, add parentEl to all nodes
|
|
||||||
* @param nodes
|
|
||||||
*/
|
|
||||||
initNewNodes(nodes) {
|
|
||||||
// ---- Add parentEl to all nodes
|
|
||||||
DLNode.addParentEl(nodes, this._$parentEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Generate new nodes in the saved environment
|
|
||||||
* @param newNodesFunc
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
geneNewNodesInEnv(newNodesFunc) {
|
|
||||||
if (!this.savedEnvNodes) {
|
|
||||||
// ---- No saved environment, just generate new nodes
|
|
||||||
const newNodes = newNodesFunc();
|
|
||||||
// ---- Only for IfNode's same condition return
|
|
||||||
// ---- Initialize the new nodes
|
|
||||||
this.initNewNodes(newNodes);
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
// ---- Save the current environment nodes
|
|
||||||
const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
|
|
||||||
// ---- Replace the saved environment nodes
|
|
||||||
DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
|
|
||||||
const newNodes = newNodesFunc();
|
|
||||||
// ---- Retrieve the current environment nodes
|
|
||||||
DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
|
|
||||||
// ---- Only for IfNode's same condition return
|
|
||||||
// ---- Initialize the new nodes
|
|
||||||
this.initNewNodes(newNodes);
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
initUnmountStore() {
|
|
||||||
DLStore.global.WillUnmountStore.push([]);
|
|
||||||
DLStore.global.DidUnmountStore.push([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Remove nodes from parentEl and run willUnmount and didUnmount
|
|
||||||
* @param nodes
|
|
||||||
* @param removeEl Only remove outermost element
|
|
||||||
*/
|
|
||||||
removeNodes(nodes) {
|
|
||||||
DLNode.loopShallowEls(nodes, node => {
|
|
||||||
this._$parentEl.removeChild(node);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { DLNodeType } from '../DLNode';
|
|
||||||
import { FlatNode } from './FlatNode';
|
|
||||||
import { EnvNode } from '../EnvNode';
|
|
||||||
|
|
||||||
export class TryNode extends FlatNode {
|
|
||||||
constructor(tryFunc, catchFunc) {
|
|
||||||
super(DLNodeType.Try);
|
|
||||||
this.tryFunc = tryFunc;
|
|
||||||
const catchable = this.getCatchable(catchFunc);
|
|
||||||
this.envNode = new EnvNode({ _$catchable: catchable });
|
|
||||||
const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
|
|
||||||
this.envNode.initNodes(nodes);
|
|
||||||
this._$nodes = nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
update(changed) {
|
|
||||||
this.updateFunc?.(changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpdateFunc(updateFunc) {
|
|
||||||
this.updateFunc = updateFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCatchable(catchFunc) {
|
|
||||||
return callback =>
|
|
||||||
(...args) => {
|
|
||||||
try {
|
|
||||||
return callback(...args);
|
|
||||||
} catch (e) {
|
|
||||||
// ---- Run it in next tick to make sure when error occurs before
|
|
||||||
// didMount, this._$parentEl is not null
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
|
|
||||||
this._$nodes && this.removeNodes(this._$nodes);
|
|
||||||
const parentEl = this._$parentEl;
|
|
||||||
const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
|
|
||||||
const nextSibling = parentEl.childNodes[flowIndex];
|
|
||||||
FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
|
|
||||||
FlatNode.runDidMount();
|
|
||||||
this._$nodes = nodes;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { insertNode } from './HTMLNode';
|
|
||||||
export class PropView {
|
|
||||||
propViewFunc;
|
|
||||||
dlUpdateFunc = new Set();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief PropView constructor, accept a function that returns a list of DLNode
|
|
||||||
* @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
|
|
||||||
*/
|
|
||||||
constructor(propViewFunc) {
|
|
||||||
this.propViewFunc = propViewFunc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
|
|
||||||
* @returns An array of DLNode instances returned by propViewFunc
|
|
||||||
*/
|
|
||||||
build() {
|
|
||||||
let update;
|
|
||||||
const addUpdate = updateFunc => {
|
|
||||||
update = updateFunc;
|
|
||||||
this.dlUpdateFunc.add(updateFunc);
|
|
||||||
};
|
|
||||||
const newNodes = this.propViewFunc(addUpdate);
|
|
||||||
if (newNodes.length === 0) return [];
|
|
||||||
if (update) {
|
|
||||||
// Remove the updateNode from dlUpdateNodes when it unmounts
|
|
||||||
DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update every node in dlUpdateNodes
|
|
||||||
* @param changed - A parameter indicating what changed to trigger the update
|
|
||||||
*/
|
|
||||||
update(...args) {
|
|
||||||
this.dlUpdateFunc.forEach(update => {
|
|
||||||
update(...args);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertChildren(el, propView) {
|
|
||||||
insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { DLNode, DLNodeType } from './DLNode';
|
|
||||||
import { cached } from './store';
|
|
||||||
|
|
||||||
export class SnippetNode extends DLNode {
|
|
||||||
constructor(depsArr) {
|
|
||||||
super(DLNodeType.Snippet);
|
|
||||||
this.depsArr = depsArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
cached(deps, changed) {
|
|
||||||
if (!deps || !deps.length) return false;
|
|
||||||
const idx = Math.log2(changed);
|
|
||||||
const prevDeps = this.depsArr[idx];
|
|
||||||
if (cached(deps, prevDeps)) return true;
|
|
||||||
this.depsArr[idx] = deps;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { DLStore, cached } from './store';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Shorten document.createTextNode
|
|
||||||
* @param value
|
|
||||||
* @returns Text
|
|
||||||
*/
|
|
||||||
export function createTextNode(value, deps) {
|
|
||||||
const node = DLStore.document.createTextNode(value);
|
|
||||||
node.$$deps = deps;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Update text node and check if the value is changed
|
|
||||||
* @param node
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
export function updateText(node, valueFunc, deps) {
|
|
||||||
if (cached(deps, node.$$deps)) return;
|
|
||||||
const value = valueFunc();
|
|
||||||
node.textContent = value;
|
|
||||||
node.$$deps = deps;
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './types/index';
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { DLNode } from './DLNode';
|
|
||||||
import { insertNode } from './HTMLNode';
|
|
||||||
|
|
||||||
export * from './HTMLNode';
|
|
||||||
export * from './CompNode';
|
|
||||||
export * from './EnvNode';
|
|
||||||
export * from './TextNode';
|
|
||||||
export * from './PropView';
|
|
||||||
export * from './SnippetNode';
|
|
||||||
export * from './MutableNode/ForNode';
|
|
||||||
export * from './MutableNode/ExpNode';
|
|
||||||
export * from './MutableNode/CondNode';
|
|
||||||
export * from './MutableNode/TryNode';
|
|
||||||
|
|
||||||
import { DLStore } from './store';
|
|
||||||
|
|
||||||
export { setGlobal, setDocument } from './store';
|
|
||||||
|
|
||||||
function initStore() {
|
|
||||||
// Declare a global variable to store willUnmount functions
|
|
||||||
DLStore.global.WillUnmountStore = [];
|
|
||||||
// Declare a global variable to store didUnmount functions
|
|
||||||
DLStore.global.DidUnmountStore = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Render the DL class to the element
|
|
||||||
* @param {typeof import('./CompNode').CompNode} Comp
|
|
||||||
* @param {HTMLElement | string} idOrEl
|
|
||||||
*/
|
|
||||||
export function render(Comp, idOrEl) {
|
|
||||||
let el = idOrEl;
|
|
||||||
if (typeof idOrEl === 'string') {
|
|
||||||
const elFound = DLStore.document.getElementById(idOrEl);
|
|
||||||
if (elFound) el = elFound;
|
|
||||||
else {
|
|
||||||
throw new Error(`DLight: Element with id ${idOrEl} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initStore();
|
|
||||||
el.innerHTML = '';
|
|
||||||
const dlNode = new Comp();
|
|
||||||
dlNode._$init();
|
|
||||||
insertNode(el, dlNode, 0);
|
|
||||||
DLNode.runDidMount();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function manual(callback, _deps) {
|
|
||||||
return callback();
|
|
||||||
}
|
|
||||||
export function escape(arg) {
|
|
||||||
return arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const $ = escape;
|
|
||||||
export const required = null;
|
|
||||||
|
|
||||||
export function use() {
|
|
||||||
console.error(
|
|
||||||
'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const p = Promise.resolve();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule a task to run in the next microtask.
|
|
||||||
*
|
|
||||||
* @param {() => void} task
|
|
||||||
*/
|
|
||||||
export function schedule(task) {
|
|
||||||
p.then(task);
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { Store } from '@openinula/store';
|
|
||||||
|
|
||||||
// ---- Using external Store to store global and document
|
|
||||||
// Because Store is a singleton, it is safe to use it as a global variable
|
|
||||||
// If created in DLight package, different package versions will introduce
|
|
||||||
// multiple Store instances.
|
|
||||||
|
|
||||||
if (!('global' in Store)) {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
Store.global = window;
|
|
||||||
} else if (typeof global !== 'undefined') {
|
|
||||||
Store.global = global;
|
|
||||||
} else {
|
|
||||||
Store.global = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!('document' in Store)) {
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
Store.document = document;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DLStore = { ...Store, delegatedEvents: new Set() };
|
|
||||||
|
|
||||||
export function setGlobal(globalObj) {
|
|
||||||
DLStore.global = globalObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDocument(customDocument) {
|
|
||||||
DLStore.document = customDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Compare the deps with the previous deps
|
|
||||||
* @param deps
|
|
||||||
* @param prevDeps
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function cached(deps, prevDeps) {
|
|
||||||
if (!prevDeps || deps.length !== prevDeps.length) return false;
|
|
||||||
return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { type DLightHTMLAttributes } from './htmlTag';
|
|
||||||
|
|
||||||
// a very magical solution
|
|
||||||
// when vscode parse ts, if it is type A<T> = B<xxx<T>>, it will show the detailed type,
|
|
||||||
// but if type A<T> = B<xxx<T>> & xxx, it will only show alias (here is A)
|
|
||||||
// because I don't want to expose the detailed type, so type A<T> = B<xxx<T>> & Useless
|
|
||||||
// but if type Useless = { useless: never } will cause this type to have an additional property userless
|
|
||||||
// so just don't add key!
|
|
||||||
type Useless = { [key in '']: never };
|
|
||||||
|
|
||||||
export type DLightObject<T> = {
|
|
||||||
[K in keyof T]-?: undefined extends T[K]
|
|
||||||
? (value?: T[K]) => DLightObject<Omit<T, K>>
|
|
||||||
: (value: T[K]) => DLightObject<Omit<T, K>>;
|
|
||||||
};
|
|
||||||
interface CustomNodeProps {
|
|
||||||
willMount: (node: any) => void;
|
|
||||||
didMount: (node: any) => void;
|
|
||||||
willUnmount: (node: any) => void;
|
|
||||||
didUnmount: (node: any) => void;
|
|
||||||
didUpdate: (node: any, key: string, prevValue: any, currValue: any) => void;
|
|
||||||
ref: (node: any) => void;
|
|
||||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
|
||||||
forwardProps: true | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContentProp<T = object> = T & { _$idContent: true };
|
|
||||||
|
|
||||||
export type RemoveOptional<T> = {
|
|
||||||
[K in keyof T]-?: T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
type IsAny<T> = { _$isAny: true } extends T ? true : false;
|
|
||||||
|
|
||||||
export type ContentKeyName<T> = {
|
|
||||||
[K in keyof T]: IsAny<T[K]> extends true
|
|
||||||
? never
|
|
||||||
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
T[K] extends ContentProp<infer _>
|
|
||||||
? K
|
|
||||||
: never;
|
|
||||||
}[keyof T];
|
|
||||||
|
|
||||||
export type CheckContent<T> = RemoveOptional<T>[ContentKeyName<RemoveOptional<T>>];
|
|
||||||
|
|
||||||
type CustomClassTag<T, O> =
|
|
||||||
ContentKeyName<RemoveOptional<O>> extends undefined
|
|
||||||
? () => DLightObject<T>
|
|
||||||
: undefined extends O[ContentKeyName<RemoveOptional<O>>]
|
|
||||||
? CheckContent<O> extends ContentProp<infer U>
|
|
||||||
? (content?: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
|
||||||
: never
|
|
||||||
: CheckContent<O> extends ContentProp<infer U>
|
|
||||||
? (content: U extends unknown ? any : unknown) => DLightObject<Omit<T, ContentKeyName<RemoveOptional<O>>>>
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type CustomSnippetTag<T> = T extends { content: infer U }
|
|
||||||
? (content: U) => DLightObject<Omit<T, 'content'>>
|
|
||||||
: T extends { content?: infer U }
|
|
||||||
? (content?: U) => DLightObject<Omit<T, 'content'>>
|
|
||||||
: () => DLightObject<T>;
|
|
||||||
|
|
||||||
type CustomTagType<T, G> = CustomClassTag<
|
|
||||||
T & CustomNodeProps & (keyof G extends never ? object : DLightHTMLAttributes<G, object, HTMLElement>),
|
|
||||||
T
|
|
||||||
> &
|
|
||||||
Useless;
|
|
||||||
export type Typed<T = object, G = object> = CustomTagType<T, G> & Useless;
|
|
||||||
export type SnippetTyped<T = object> = CustomSnippetTag<T> & Useless;
|
|
||||||
|
|
||||||
export type Pretty = any;
|
|
||||||
|
|
||||||
// ---- reverse
|
|
||||||
export type UnTyped<T> = T extends Typed<infer U> ? U : never;
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// ---- env
|
|
||||||
import { DLightObject } from './compTag';
|
|
||||||
|
|
||||||
type AnyEnv = { _$anyEnv: true };
|
|
||||||
|
|
||||||
export const env: <T = AnyEnv>() => T extends AnyEnv ? any : DLightObject<T>;
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
interface ExpressionTag {
|
|
||||||
willMount: (node: any) => void;
|
|
||||||
didMount: (node: any) => void;
|
|
||||||
willUnmount: (node: any) => void;
|
|
||||||
didUnmount: (node: any) => void;
|
|
||||||
didUpdate: <T>(node: any, key: string, prevValue: T, currValue: T) => void;
|
|
||||||
elements: HTMLElement[] | ((holder: HTMLElement[]) => void) | undefined;
|
|
||||||
ref: (node: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExpressionTagFunc = (nodes: any) => ExpressionTag;
|
|
||||||
|
|
||||||
export const _: ExpressionTagFunc;
|
|
||||||
|
|
@ -1,516 +0,0 @@
|
||||||
export interface DLightGlobalEventHandlers {
|
|
||||||
/**
|
|
||||||
* Fires when the user aborts the download.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/abort_event)
|
|
||||||
*/
|
|
||||||
onAbort: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationcancel_event) */
|
|
||||||
onAnimationCancel: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event) */
|
|
||||||
onAnimationEnd: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event) */
|
|
||||||
onAnimationIteration: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event) */
|
|
||||||
onAnimationStart: ((this: GlobalEventHandlers, ev: AnimationEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/auxclick_event) */
|
|
||||||
onAuxClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/beforeinput_event) */
|
|
||||||
onBeforeInput: ((this: GlobalEventHandlers, ev: InputEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the object loses the input focus.
|
|
||||||
* @param ev The focus event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/blur_event)
|
|
||||||
*/
|
|
||||||
onBlur: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/cancel_event) */
|
|
||||||
onCancel: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback is possible, but would require further buffering.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplay_event)
|
|
||||||
*/
|
|
||||||
onCanPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/canplaythrough_event) */
|
|
||||||
onCanPlayThrough: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the contents of the object or selection have changed.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/change_event)
|
|
||||||
*/
|
|
||||||
onChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the left mouse button on the object
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/click_event)
|
|
||||||
*/
|
|
||||||
onClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDialogElement/close_event) */
|
|
||||||
onClose: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the right mouse button in the client area, opening the context menu.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/contextmenu_event)
|
|
||||||
*/
|
|
||||||
onContextMenu: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/copy_event) */
|
|
||||||
onCopy: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLTrackElement/cuechange_event) */
|
|
||||||
onCueChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/cut_event) */
|
|
||||||
onCut: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user double-clicks the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/dblclick_event)
|
|
||||||
*/
|
|
||||||
onDblClick: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object continuously during a drag operation.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drag_event)
|
|
||||||
*/
|
|
||||||
onDrag: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object when the user releases the mouse at the close of a drag operation.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragend_event)
|
|
||||||
*/
|
|
||||||
onDragEnd: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target element when the user drags the object to a valid drop target.
|
|
||||||
* @param ev The drag event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragenter_event)
|
|
||||||
*/
|
|
||||||
onDragEnter: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target object when the user moves the mouse out of a valid drop target during a drag operation.
|
|
||||||
* @param ev The drag event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragleave_event)
|
|
||||||
*/
|
|
||||||
onDragLeave: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the target element continuously while the user drags the object over a valid drop target.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragover_event)
|
|
||||||
*/
|
|
||||||
onDragOver: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires on the source object when the user starts to drag a text selection or selected object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/dragstart_event)
|
|
||||||
*/
|
|
||||||
onDragStart: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/drop_event) */
|
|
||||||
onDrop: ((this: GlobalEventHandlers, ev: DragEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the duration attribute is updated.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/durationchange_event)
|
|
||||||
*/
|
|
||||||
onDurationChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the media element is reset to its initial state.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/emptied_event)
|
|
||||||
*/
|
|
||||||
onEmptied: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the end of playback is reached.
|
|
||||||
* @param ev The event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ended_event)
|
|
||||||
*/
|
|
||||||
onEnded: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when an error occurs during object loading.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/error_event)
|
|
||||||
*/
|
|
||||||
onError: OnErrorEventHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the object receives focus.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/focus_event)
|
|
||||||
*/
|
|
||||||
onFocus: ((this: GlobalEventHandlers, ev: FocusEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/formdata_event) */
|
|
||||||
onFormData: ((this: GlobalEventHandlers, ev: FormDataEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/gotpointercapture_event) */
|
|
||||||
onGotPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/input_event) */
|
|
||||||
onInput: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/invalid_event) */
|
|
||||||
onInvalid: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user presses a key.
|
|
||||||
* @param ev The keyboard event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keydown_event)
|
|
||||||
*/
|
|
||||||
onKeyDown: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user presses an alphanumeric key.
|
|
||||||
* @param ev The event.
|
|
||||||
* @deprecated
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keypress_event)
|
|
||||||
*/
|
|
||||||
onKeyPress: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user releases a key.
|
|
||||||
* @param ev The keyboard event
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/keyup_event)
|
|
||||||
*/
|
|
||||||
onKeyUp: ((this: GlobalEventHandlers, ev: KeyboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires immediately after the browser loads the object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SVGElement/load_event)
|
|
||||||
*/
|
|
||||||
onLoad: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when media data is loaded at the current playback position.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadeddata_event)
|
|
||||||
*/
|
|
||||||
onLoadedData: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the duration and dimensions of the media have been determined.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadedmetadata_event)
|
|
||||||
*/
|
|
||||||
onLoadedMetadata: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when Internet Explorer begins looking for media data.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/loadstart_event)
|
|
||||||
*/
|
|
||||||
onLoadStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/lostpointercapture_event) */
|
|
||||||
onLostPointerCapture: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user clicks the object with either mouse button.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousedown_event)
|
|
||||||
*/
|
|
||||||
onMouseDown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseenter_event) */
|
|
||||||
onMouseEnter: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseleave_event) */
|
|
||||||
onMouseLeave: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse over the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mousemove_event)
|
|
||||||
*/
|
|
||||||
onMouseMove: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse pointer outside the boundaries of the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseout_event)
|
|
||||||
*/
|
|
||||||
onMouseOut: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user moves the mouse pointer into the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseover_event)
|
|
||||||
*/
|
|
||||||
onMouseOver: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user releases a mouse button while the mouse is over the object.
|
|
||||||
* @param ev The mouse event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/mouseup_event)
|
|
||||||
*/
|
|
||||||
onMouseUp: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/paste_event) */
|
|
||||||
onPaste: ((this: GlobalEventHandlers, ev: ClipboardEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback is paused.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/pause_event)
|
|
||||||
*/
|
|
||||||
onPause: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the play method is requested.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play_event)
|
|
||||||
*/
|
|
||||||
onPlay: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the audio or video has started playing.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/playing_event)
|
|
||||||
*/
|
|
||||||
onPlaying: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointercancel_event) */
|
|
||||||
onPointerCancel: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerdown_event) */
|
|
||||||
onPointerDown: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerenter_event) */
|
|
||||||
onPointerEnter: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerleave_event) */
|
|
||||||
onPointerLeave: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointermove_event) */
|
|
||||||
onPointerMove: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerout_event) */
|
|
||||||
onPointerOut: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerover_event) */
|
|
||||||
onPointerOver: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/pointerup_event) */
|
|
||||||
onPointerUp: ((this: GlobalEventHandlers, ev: PointerEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs to indicate progress while downloading media data.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/progress_event)
|
|
||||||
*/
|
|
||||||
onProgress: ((this: GlobalEventHandlers, ev: ProgressEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the playback rate is increased or decreased.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/ratechange_event)
|
|
||||||
*/
|
|
||||||
onRateChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user resets a form.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/reset_event)
|
|
||||||
*/
|
|
||||||
onReset: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLVideoElement/resize_event) */
|
|
||||||
onResize: ((this: GlobalEventHandlers, ev: UIEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the user repositions the scroll box in the scroll bar on the object.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scroll_event)
|
|
||||||
*/
|
|
||||||
onScroll: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/scrollend_event) */
|
|
||||||
onScrollEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/securitypolicyviolation_event) */
|
|
||||||
onSecurityPolicyViolation: ((this: GlobalEventHandlers, ev: SecurityPolicyViolationEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the seek operation ends.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeked_event)
|
|
||||||
*/
|
|
||||||
onSeeked: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the current playback position is moved.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/seeking_event)
|
|
||||||
*/
|
|
||||||
onSeeking: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fires when the current selection changes.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/select_event)
|
|
||||||
*/
|
|
||||||
onSelect: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/selectionchange_event) */
|
|
||||||
onSelectionChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/selectstart_event) */
|
|
||||||
onSelectStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLSlotElement/slotchange_event) */
|
|
||||||
onSlotChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the download has stopped.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/stalled_event)
|
|
||||||
*/
|
|
||||||
onStalled: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLFormElement/submit_event) */
|
|
||||||
onSubmit: ((this: GlobalEventHandlers, ev: SubmitEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs if the load operation has been intentionally halted.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/suspend_event)
|
|
||||||
*/
|
|
||||||
onSuspend: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs to indicate the current playback position.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/timeupdate_event)
|
|
||||||
*/
|
|
||||||
onTimeUpdate: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLDetailsElement/toggle_event) */
|
|
||||||
onToggle: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitioncancel_event) */
|
|
||||||
onTransitionCancel: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event) */
|
|
||||||
onTransitionEnd: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionrun_event) */
|
|
||||||
onTransitionRun: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionstart_event) */
|
|
||||||
onTransitionStart: ((this: GlobalEventHandlers, ev: TransitionEvent) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when the volume is changed, or playback is muted or unmuted.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/volumechange_event)
|
|
||||||
*/
|
|
||||||
onVolumeChange: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when playback stops because the next frame of a video resource is not available.
|
|
||||||
* @param ev The event.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/waiting_event)
|
|
||||||
*/
|
|
||||||
onWaiting: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationEnd`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationend_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationIteration`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationiteration_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationIteration: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onAnimationStart`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/animationstart_event)
|
|
||||||
*/
|
|
||||||
onWebkitAnimationStart: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated This is a legacy alias of `onTransitionEnd`.
|
|
||||||
*
|
|
||||||
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/transitionend_event)
|
|
||||||
*/
|
|
||||||
onWebkitTransitionEnd: ((this: GlobalEventHandlers, ev: Event) => any) | null;
|
|
||||||
|
|
||||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/wheel_event) */
|
|
||||||
onWheel: ((this: GlobalEventHandlers, ev: WheelEvent) => any) | null;
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { type Properties } from 'csstype';
|
|
||||||
|
|
||||||
// ---- Used to determine whether X and Y are equal, return A if equal, otherwise B
|
|
||||||
type IfEquals<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
|
|
||||||
|
|
||||||
export type OmitIndexSignature<ObjectType> = {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown> ? never : KeyType]: ObjectType[KeyType];
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- For each key, check whether there is readonly, if there is, return never, and then Pick out is not never
|
|
||||||
type WritableKeysOf<T> = {
|
|
||||||
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>;
|
|
||||||
}[keyof T];
|
|
||||||
type RemoveReadOnly<T> = Pick<T, WritableKeysOf<T>>;
|
|
||||||
|
|
||||||
// ---- Delete all functions
|
|
||||||
type OmitFunction<T> = Omit<T, { [K in keyof T]: T[K] extends (...args: any) => any ? K : never }[keyof T]>;
|
|
||||||
|
|
||||||
type OmitFuncAndReadOnly<T> = RemoveReadOnly<OmitFunction<OmitIndexSignature<T>>>;
|
|
||||||
|
|
||||||
// ---- properties
|
|
||||||
type OmitFuncAndReadOnlyProperty<G> = Omit<OmitFuncAndReadOnly<G>, 'className' | 'htmlFor' | 'style' | 'innerText'>;
|
|
||||||
|
|
||||||
type CustomCSSProperties = {
|
|
||||||
[Key in `--${string}`]: string | number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HTMLAttributes<T> = OmitFuncAndReadOnlyProperty<T> & {
|
|
||||||
style: Properties & CustomCSSProperties;
|
|
||||||
class: string;
|
|
||||||
for: string;
|
|
||||||
};
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
import type { DLightGlobalEventHandlers } from './event';
|
|
||||||
import type { OmitIndexSignature, HTMLAttributes } from './htmlElement';
|
|
||||||
|
|
||||||
// ---- If there is an event(start with on), remove it
|
|
||||||
export type PropertyWithEvent<G> = Omit<
|
|
||||||
G,
|
|
||||||
{
|
|
||||||
[K in keyof G]: K extends `on${string}` ? K : never;
|
|
||||||
}[keyof G]
|
|
||||||
> &
|
|
||||||
DLightGlobalEventHandlers;
|
|
||||||
|
|
||||||
interface DLightHtmlProps<El> {
|
|
||||||
ref: El | ((holder: El) => void) | undefined;
|
|
||||||
prop: Record<string, string | number | boolean>;
|
|
||||||
attr: Record<string, string>;
|
|
||||||
dataset: Record<string, string>;
|
|
||||||
forwardProps: true | undefined;
|
|
||||||
willMount: (el: El) => void;
|
|
||||||
didMount: (el: El) => void;
|
|
||||||
willUnmount: (el: El) => void;
|
|
||||||
didUnmount: (el: El) => void;
|
|
||||||
didUpdate: <T>(el: El, key: string, prevValue: T, currValue: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DLightHTMLAttributes<T, G, El> = DLightHtmlProps<El> & HTMLAttributes<T> & G;
|
|
||||||
|
|
||||||
export type DLightHTMLAttributesFunc<T, G, El> = {
|
|
||||||
[K in keyof DLightHTMLAttributes<T, G, El>]: (
|
|
||||||
value?: DLightHTMLAttributes<T, G, El>[K]
|
|
||||||
) => Omit<DLightHTMLAttributesFunc<T, G, El>, K>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DLightHtmlTagFunc<T = HTMLElement, G = object> = (
|
|
||||||
innerText?: string | number | ((View: never) => void)
|
|
||||||
) => DLightHTMLAttributesFunc<PropertyWithEvent<OmitIndexSignature<T>>, G, T>;
|
|
||||||
|
|
||||||
export const a: DLightHtmlTagFunc<HTMLAnchorElement>;
|
|
||||||
export const abbr: DLightHtmlTagFunc;
|
|
||||||
export const address: DLightHtmlTagFunc;
|
|
||||||
export const area: DLightHtmlTagFunc<HTMLAreaElement>;
|
|
||||||
export const article: DLightHtmlTagFunc;
|
|
||||||
export const aside: DLightHtmlTagFunc;
|
|
||||||
export const audio: DLightHtmlTagFunc<HTMLAudioElement>;
|
|
||||||
export const b: DLightHtmlTagFunc;
|
|
||||||
export const base: DLightHtmlTagFunc<HTMLBaseElement>;
|
|
||||||
export const bdi: DLightHtmlTagFunc;
|
|
||||||
export const bdo: DLightHtmlTagFunc;
|
|
||||||
export const blockquote: DLightHtmlTagFunc<HTMLQuoteElement>;
|
|
||||||
export const body: DLightHtmlTagFunc<HTMLBodyElement>;
|
|
||||||
export const br: DLightHtmlTagFunc<HTMLBRElement>;
|
|
||||||
export const button: DLightHtmlTagFunc<HTMLButtonElement>;
|
|
||||||
export const canvas: DLightHtmlTagFunc<HTMLCanvasElement>;
|
|
||||||
export const caption: DLightHtmlTagFunc<HTMLTableCaptionElement>;
|
|
||||||
export const cite: DLightHtmlTagFunc;
|
|
||||||
export const code: DLightHtmlTagFunc;
|
|
||||||
export const col: DLightHtmlTagFunc<HTMLTableColElement>;
|
|
||||||
export const colgroup: DLightHtmlTagFunc<HTMLTableColElement>;
|
|
||||||
export const data: DLightHtmlTagFunc<HTMLDataElement>;
|
|
||||||
export const datalist: DLightHtmlTagFunc<HTMLDataListElement>;
|
|
||||||
export const dd: DLightHtmlTagFunc;
|
|
||||||
export const del: DLightHtmlTagFunc<HTMLModElement>;
|
|
||||||
export const details: DLightHtmlTagFunc<HTMLDetailsElement>;
|
|
||||||
export const dfn: DLightHtmlTagFunc;
|
|
||||||
export const dialog: DLightHtmlTagFunc<HTMLDialogElement>;
|
|
||||||
export const div: DLightHtmlTagFunc<HTMLDivElement>;
|
|
||||||
export const dl: DLightHtmlTagFunc<HTMLDListElement>;
|
|
||||||
export const dt: DLightHtmlTagFunc;
|
|
||||||
export const em: DLightHtmlTagFunc;
|
|
||||||
export const embed: DLightHtmlTagFunc<HTMLEmbedElement>;
|
|
||||||
export const fieldset: DLightHtmlTagFunc<HTMLFieldSetElement>;
|
|
||||||
export const figcaption: DLightHtmlTagFunc;
|
|
||||||
export const figure: DLightHtmlTagFunc;
|
|
||||||
export const footer: DLightHtmlTagFunc;
|
|
||||||
export const form: DLightHtmlTagFunc<HTMLFormElement>;
|
|
||||||
export const h1: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h2: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h3: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h4: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h5: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const h6: DLightHtmlTagFunc<HTMLHeadingElement>;
|
|
||||||
export const head: DLightHtmlTagFunc<HTMLHeadElement>;
|
|
||||||
export const header: DLightHtmlTagFunc;
|
|
||||||
export const hgroup: DLightHtmlTagFunc;
|
|
||||||
export const hr: DLightHtmlTagFunc<HTMLHRElement>;
|
|
||||||
export const html: DLightHtmlTagFunc<HTMLHtmlElement>;
|
|
||||||
export const i: DLightHtmlTagFunc;
|
|
||||||
export const iframe: DLightHtmlTagFunc<HTMLIFrameElement>;
|
|
||||||
export const img: DLightHtmlTagFunc<HTMLImageElement>;
|
|
||||||
export const input: DLightHtmlTagFunc<HTMLInputElement>;
|
|
||||||
export const ins: DLightHtmlTagFunc<HTMLModElement>;
|
|
||||||
export const kbd: DLightHtmlTagFunc;
|
|
||||||
export const label: DLightHtmlTagFunc<HTMLLabelElement>;
|
|
||||||
export const legend: DLightHtmlTagFunc<HTMLLegendElement>;
|
|
||||||
export const li: DLightHtmlTagFunc<HTMLLIElement>;
|
|
||||||
export const link: DLightHtmlTagFunc<HTMLLinkElement>;
|
|
||||||
export const main: DLightHtmlTagFunc;
|
|
||||||
export const map: DLightHtmlTagFunc<HTMLMapElement>;
|
|
||||||
export const mark: DLightHtmlTagFunc;
|
|
||||||
export const menu: DLightHtmlTagFunc<HTMLMenuElement>;
|
|
||||||
export const meta: DLightHtmlTagFunc<HTMLMetaElement>;
|
|
||||||
export const meter: DLightHtmlTagFunc<HTMLMeterElement>;
|
|
||||||
export const nav: DLightHtmlTagFunc;
|
|
||||||
export const noscript: DLightHtmlTagFunc;
|
|
||||||
export const object: DLightHtmlTagFunc<HTMLObjectElement>;
|
|
||||||
export const ol: DLightHtmlTagFunc<HTMLOListElement>;
|
|
||||||
export const optgroup: DLightHtmlTagFunc<HTMLOptGroupElement>;
|
|
||||||
export const option: DLightHtmlTagFunc<HTMLOptionElement>;
|
|
||||||
export const output: DLightHtmlTagFunc<HTMLOutputElement>;
|
|
||||||
export const p: DLightHtmlTagFunc<HTMLParagraphElement>;
|
|
||||||
export const picture: DLightHtmlTagFunc<HTMLPictureElement>;
|
|
||||||
export const pre: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const progress: DLightHtmlTagFunc<HTMLProgressElement>;
|
|
||||||
export const q: DLightHtmlTagFunc<HTMLQuoteElement>;
|
|
||||||
export const rp: DLightHtmlTagFunc;
|
|
||||||
export const rt: DLightHtmlTagFunc;
|
|
||||||
export const ruby: DLightHtmlTagFunc;
|
|
||||||
export const s: DLightHtmlTagFunc;
|
|
||||||
export const samp: DLightHtmlTagFunc;
|
|
||||||
export const script: DLightHtmlTagFunc<HTMLScriptElement>;
|
|
||||||
export const section: DLightHtmlTagFunc;
|
|
||||||
export const select: DLightHtmlTagFunc<HTMLSelectElement>;
|
|
||||||
export const slot: DLightHtmlTagFunc<HTMLSlotElement>;
|
|
||||||
export const small: DLightHtmlTagFunc;
|
|
||||||
export const source: DLightHtmlTagFunc<HTMLSourceElement>;
|
|
||||||
export const span: DLightHtmlTagFunc<HTMLSpanElement>;
|
|
||||||
export const strong: DLightHtmlTagFunc;
|
|
||||||
export const style: DLightHtmlTagFunc<HTMLStyleElement>;
|
|
||||||
export const sub: DLightHtmlTagFunc;
|
|
||||||
export const summary: DLightHtmlTagFunc;
|
|
||||||
export const sup: DLightHtmlTagFunc;
|
|
||||||
export const table: DLightHtmlTagFunc<HTMLTableElement>;
|
|
||||||
export const tbody: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const td: DLightHtmlTagFunc<HTMLTableCellElement>;
|
|
||||||
export const template: DLightHtmlTagFunc<HTMLTemplateElement>;
|
|
||||||
export const textarea: DLightHtmlTagFunc<HTMLTextAreaElement>;
|
|
||||||
export const tfoot: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const th: DLightHtmlTagFunc<HTMLTableCellElement>;
|
|
||||||
export const thead: DLightHtmlTagFunc<HTMLTableSectionElement>;
|
|
||||||
export const time: DLightHtmlTagFunc<HTMLTimeElement>;
|
|
||||||
export const title: DLightHtmlTagFunc<HTMLTitleElement>;
|
|
||||||
export const tr: DLightHtmlTagFunc<HTMLTableRowElement>;
|
|
||||||
export const track: DLightHtmlTagFunc<HTMLTrackElement>;
|
|
||||||
export const u: DLightHtmlTagFunc;
|
|
||||||
export const ul: DLightHtmlTagFunc<HTMLUListElement>;
|
|
||||||
export const var_: DLightHtmlTagFunc;
|
|
||||||
export const video: DLightHtmlTagFunc<HTMLVideoElement>;
|
|
||||||
export const wbr: DLightHtmlTagFunc;
|
|
||||||
export const acronym: DLightHtmlTagFunc;
|
|
||||||
export const applet: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const basefont: DLightHtmlTagFunc;
|
|
||||||
export const bgsound: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const big: DLightHtmlTagFunc;
|
|
||||||
export const blink: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const center: DLightHtmlTagFunc;
|
|
||||||
export const dir: DLightHtmlTagFunc<HTMLDirectoryElement>;
|
|
||||||
export const font: DLightHtmlTagFunc<HTMLFontElement>;
|
|
||||||
export const frame: DLightHtmlTagFunc<HTMLFrameElement>;
|
|
||||||
export const frameset: DLightHtmlTagFunc<HTMLFrameSetElement>;
|
|
||||||
export const isindex: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const keygen: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const listing: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const marquee: DLightHtmlTagFunc<HTMLMarqueeElement>;
|
|
||||||
export const menuitem: DLightHtmlTagFunc;
|
|
||||||
export const multicol: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const nextid: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const nobr: DLightHtmlTagFunc;
|
|
||||||
export const noembed: DLightHtmlTagFunc;
|
|
||||||
export const noframes: DLightHtmlTagFunc;
|
|
||||||
export const param: DLightHtmlTagFunc<HTMLParamElement>;
|
|
||||||
export const plaintext: DLightHtmlTagFunc;
|
|
||||||
export const rb: DLightHtmlTagFunc;
|
|
||||||
export const rtc: DLightHtmlTagFunc;
|
|
||||||
export const spacer: DLightHtmlTagFunc<HTMLUnknownElement>;
|
|
||||||
export const strike: DLightHtmlTagFunc;
|
|
||||||
export const tt: DLightHtmlTagFunc;
|
|
||||||
export const xmp: DLightHtmlTagFunc<HTMLPreElement>;
|
|
||||||
export const animate: DLightHtmlTagFunc<SVGAnimateElement>;
|
|
||||||
export const animateMotion: DLightHtmlTagFunc<SVGAnimateMotionElement>;
|
|
||||||
export const animateTransform: DLightHtmlTagFunc<SVGAnimateTransformElement>;
|
|
||||||
export const circle: DLightHtmlTagFunc<SVGCircleElement>;
|
|
||||||
export const clipPath: DLightHtmlTagFunc<SVGClipPathElement>;
|
|
||||||
export const defs: DLightHtmlTagFunc<SVGDefsElement>;
|
|
||||||
export const desc: DLightHtmlTagFunc<SVGDescElement>;
|
|
||||||
export const ellipse: DLightHtmlTagFunc<SVGEllipseElement>;
|
|
||||||
export const feBlend: DLightHtmlTagFunc<SVGFEBlendElement>;
|
|
||||||
export const feColorMatrix: DLightHtmlTagFunc<SVGFEColorMatrixElement>;
|
|
||||||
export const feComponentTransfer: DLightHtmlTagFunc<SVGFEComponentTransferElement>;
|
|
||||||
export const feComposite: DLightHtmlTagFunc<SVGFECompositeElement>;
|
|
||||||
export const feConvolveMatrix: DLightHtmlTagFunc<SVGFEConvolveMatrixElement>;
|
|
||||||
export const feDiffuseLighting: DLightHtmlTagFunc<SVGFEDiffuseLightingElement>;
|
|
||||||
export const feDisplacementMap: DLightHtmlTagFunc<SVGFEDisplacementMapElement>;
|
|
||||||
export const feDistantLight: DLightHtmlTagFunc<SVGFEDistantLightElement>;
|
|
||||||
export const feDropShadow: DLightHtmlTagFunc<SVGFEDropShadowElement>;
|
|
||||||
export const feFlood: DLightHtmlTagFunc<SVGFEFloodElement>;
|
|
||||||
export const feFuncA: DLightHtmlTagFunc<SVGFEFuncAElement>;
|
|
||||||
export const feFuncB: DLightHtmlTagFunc<SVGFEFuncBElement>;
|
|
||||||
export const feFuncG: DLightHtmlTagFunc<SVGFEFuncGElement>;
|
|
||||||
export const feFuncR: DLightHtmlTagFunc<SVGFEFuncRElement>;
|
|
||||||
export const feGaussianBlur: DLightHtmlTagFunc<SVGFEGaussianBlurElement>;
|
|
||||||
export const feImage: DLightHtmlTagFunc<SVGFEImageElement>;
|
|
||||||
export const feMerge: DLightHtmlTagFunc<SVGFEMergeElement>;
|
|
||||||
export const feMergeNode: DLightHtmlTagFunc<SVGFEMergeNodeElement>;
|
|
||||||
export const feMorphology: DLightHtmlTagFunc<SVGFEMorphologyElement>;
|
|
||||||
export const feOffset: DLightHtmlTagFunc<SVGFEOffsetElement>;
|
|
||||||
export const fePointLight: DLightHtmlTagFunc<SVGFEPointLightElement>;
|
|
||||||
export const feSpecularLighting: DLightHtmlTagFunc<SVGFESpecularLightingElement>;
|
|
||||||
export const feSpotLight: DLightHtmlTagFunc<SVGFESpotLightElement>;
|
|
||||||
export const feTile: DLightHtmlTagFunc<SVGFETileElement>;
|
|
||||||
export const feTurbulence: DLightHtmlTagFunc<SVGFETurbulenceElement>;
|
|
||||||
export const filter: DLightHtmlTagFunc<SVGFilterElement>;
|
|
||||||
export const foreignObject: DLightHtmlTagFunc<SVGForeignObjectElement>;
|
|
||||||
export const g: DLightHtmlTagFunc<SVGGElement>;
|
|
||||||
export const image: DLightHtmlTagFunc<SVGImageElement>;
|
|
||||||
export const line: DLightHtmlTagFunc<SVGLineElement>;
|
|
||||||
export const linearGradient: DLightHtmlTagFunc<SVGLinearGradientElement>;
|
|
||||||
export const marker: DLightHtmlTagFunc<SVGMarkerElement>;
|
|
||||||
export const mask: DLightHtmlTagFunc<SVGMaskElement>;
|
|
||||||
export const metadata: DLightHtmlTagFunc<SVGMetadataElement>;
|
|
||||||
export const mpath: DLightHtmlTagFunc<SVGMPathElement>;
|
|
||||||
export const path: DLightHtmlTagFunc<SVGPathElement>;
|
|
||||||
export const pattern: DLightHtmlTagFunc<SVGPatternElement>;
|
|
||||||
export const polygon: DLightHtmlTagFunc<SVGPolygonElement>;
|
|
||||||
export const polyline: DLightHtmlTagFunc<SVGPolylineElement>;
|
|
||||||
export const radialGradient: DLightHtmlTagFunc<SVGRadialGradientElement>;
|
|
||||||
export const rect: DLightHtmlTagFunc<SVGRectElement>;
|
|
||||||
export const set: DLightHtmlTagFunc<SVGSetElement>;
|
|
||||||
export const stop: DLightHtmlTagFunc<SVGStopElement>;
|
|
||||||
export const svg: DLightHtmlTagFunc<SVGSVGElement>;
|
|
||||||
export const switch_: DLightHtmlTagFunc<SVGSwitchElement>;
|
|
||||||
export const symbol: DLightHtmlTagFunc<SVGSymbolElement>;
|
|
||||||
export const text: DLightHtmlTagFunc<SVGTextElement>;
|
|
||||||
export const textPath: DLightHtmlTagFunc<SVGTextPathElement>;
|
|
||||||
export const tspan: DLightHtmlTagFunc<SVGTSpanElement>;
|
|
||||||
// export const use: DLightHtmlTagFunc<SVGUseElement>
|
|
||||||
export const view: DLightHtmlTagFunc<SVGViewElement>;
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { type Typed } from './compTag';
|
|
||||||
import { type DLightHtmlTagFunc } from './htmlTag';
|
|
||||||
export { type Properties as CSSProperties } from 'csstype';
|
|
||||||
|
|
||||||
export const comp: <T>(tag: T) => object extends T ? any : Typed<T>;
|
|
||||||
export const tag: (tag: any) => DLightHtmlTagFunc;
|
|
||||||
|
|
||||||
export { _ } from './expressionTag';
|
|
||||||
export * from './htmlTag';
|
|
||||||
export * from './compTag';
|
|
||||||
export * from './envTag';
|
|
||||||
export * from './model';
|
|
||||||
export const Static: any;
|
|
||||||
export const Children: any;
|
|
||||||
export const Content: any;
|
|
||||||
export const Prop: any;
|
|
||||||
export const Env: any;
|
|
||||||
export const Watch: any;
|
|
||||||
export const ForwardProps: any;
|
|
||||||
export const Main: any;
|
|
||||||
export const App: any;
|
|
||||||
export const Mount: (idOrEl: string | HTMLElement) => any;
|
|
||||||
|
|
||||||
// ---- With actual value
|
|
||||||
export function render(DL: any, idOrEl: string | HTMLElement): void;
|
|
||||||
export function manual<T>(callback: () => T, _deps?: any[]): T;
|
|
||||||
export function escape<T>(arg: T): T;
|
|
||||||
export function setGlobal(globalObj: any): void;
|
|
||||||
export function setDocument(customDocument: any): void;
|
|
||||||
export const $: typeof escape;
|
|
||||||
export const View: any;
|
|
||||||
export const Snippet: any;
|
|
||||||
export const Model: any;
|
|
||||||
export const update: any;
|
|
||||||
export const required: any;
|
|
||||||
export function insertChildren<T>(parent: T, children: DLightViewProp): void;
|
|
||||||
|
|
||||||
// ---- View types
|
|
||||||
export type DLightViewComp<T = any> = Typed<T>;
|
|
||||||
export type DLightViewProp = (View: any) => void;
|
|
||||||
export type DLightViewLazy<T = any> = () => Promise<{ default: T }>;
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { ContentKeyName, ContentProp } from './compTag';
|
|
||||||
|
|
||||||
type RemoveDLightInternal<T, Props> = Omit<T, 'willMount' | 'didMount' | 'didUpdate' | 'willUnmount' | keyof Props>;
|
|
||||||
|
|
||||||
export type Modeling<Model, Props = object> = (props: Props) => Model;
|
|
||||||
|
|
||||||
type GetProps<T> = keyof T extends never ? never : ContentKeyName<T> extends undefined ? T : Omit<T, ContentKeyName<T>>;
|
|
||||||
|
|
||||||
type GetContent<T> =
|
|
||||||
ContentKeyName<T> extends undefined ? never : T[ContentKeyName<T>] extends ContentProp<infer U> ? U : never;
|
|
||||||
|
|
||||||
export const use: <M>(
|
|
||||||
model: M,
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
props?: GetProps<Parameters<M>[0]>,
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
content?: GetContent<Parameters<M>[0]>
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
) => RemoveDLightInternal<ReturnType<M>, Parameters<M>[0]>;
|
|
||||||
|
|
||||||
// @ts-expect-error Model should be a function
|
|
||||||
export type ModelType<T> = RemoveDLightInternal<ReturnType<T>, Parameters<T>[0]>;
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
|
||||||
import { domTest as it } from './utils';
|
|
||||||
import { render, View } from '../src';
|
|
||||||
|
|
||||||
describe('components', () => {
|
|
||||||
describe('ref', () => {
|
|
||||||
it('should support ref', ({ container }) => {
|
|
||||||
let ref: HTMLElement;
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let count = 0;
|
|
||||||
let _ref: HTMLElement;
|
|
||||||
|
|
||||||
didMount: {
|
|
||||||
ref = _ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div ref={_ref}>test</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
|
|
||||||
expect(ref).toBeInstanceOf(HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support ref with function', ({ container }) => {
|
|
||||||
const fn = vi.fn();
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const ref = (el: HTMLElement) => {
|
|
||||||
fn();
|
|
||||||
expect(el).toBeInstanceOf(HTMLElement);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div ref={ref}>test</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(fn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('env', () => {
|
|
||||||
it('should support env', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<env theme="dark">
|
|
||||||
<Child name="child" />
|
|
||||||
</env>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Child({ name }, { theme }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
name is {name}, theme is {theme}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container.innerHTML).toBe('<div>name is child, theme is dark</div>');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
|
||||||
import { domTest as it } from './utils';
|
|
||||||
import { render, View } from '../src';
|
|
||||||
|
|
||||||
vi.mock('../src/scheduler', async () => {
|
|
||||||
return {
|
|
||||||
schedule: (task: () => void) => {
|
|
||||||
task();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('conditional rendering', () => {
|
|
||||||
it('should if, else, else if', ({ container }) => {
|
|
||||||
let set: (num: number) => void;
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let count = 2;
|
|
||||||
willMount: {
|
|
||||||
set = (val: number) => {
|
|
||||||
count = val;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<if cond={count > 1}>{count} is bigger than is 1</if>
|
|
||||||
<else-if cond={count === 1}>{count} is equal to 1</else-if>
|
|
||||||
<else>{count} is smaller than 1</else>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container.innerHTML).toBe('2 is bigger than is 1');
|
|
||||||
set(1);
|
|
||||||
expect(container.innerHTML).toBe('1 is equal to 1');
|
|
||||||
set(0);
|
|
||||||
expect(container.innerHTML).toBe('0 is smaller than 1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support nested if', ({ container }) => {
|
|
||||||
let set: (num: number) => void;
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
let count = 0;
|
|
||||||
willMount: {
|
|
||||||
set = (val: number) => {
|
|
||||||
count = val;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<if cond={count > 1}>
|
|
||||||
{count} is bigger than is 1
|
|
||||||
<if cond={count > 2}>
|
|
||||||
<div>{count} is bigger than is 2</div>
|
|
||||||
</if>
|
|
||||||
</if>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`""`);
|
|
||||||
set(2);
|
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
||||||
"2 is bigger than is 1
|
|
||||||
"
|
|
||||||
`);
|
|
||||||
set(3);
|
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
||||||
"3 is bigger than is 1
|
|
||||||
<div>3 is bigger than is 2</div>"
|
|
||||||
`);
|
|
||||||
set(2);
|
|
||||||
expect(container.innerHTML).toMatchInlineSnapshot(`
|
|
||||||
"2 is bigger than is 1
|
|
||||||
"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
|
||||||
import { domTest as it } from './utils';
|
|
||||||
import { render, View } from '../src';
|
|
||||||
|
|
||||||
describe('lifecycle', () => {
|
|
||||||
it('should call willMount', ({ container }) => {
|
|
||||||
const fn = vi.fn();
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
willMount: {
|
|
||||||
expect(container.innerHTML).toBe('');
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>test</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(fn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call didMount', ({ container }) => {
|
|
||||||
const fn = vi.fn();
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
didMount: {
|
|
||||||
expect(container.innerHTML).toBe('<div>test</div>');
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>test</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(fn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: implement unmount
|
|
||||||
it.skip('should call willUnmount', ({ container }) => {
|
|
||||||
const fn = vi.fn();
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
willUnmount: {
|
|
||||||
expect(container.innerHTML).toBe('<div>test</div>');
|
|
||||||
fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>test</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(fn).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
|
||||||
import { domTest as it } from './utils';
|
|
||||||
import { render, View } from '../src';
|
|
||||||
|
|
||||||
describe('props', () => {
|
|
||||||
describe('normal props', () => {
|
|
||||||
it('should support prop', ({ container }) => {
|
|
||||||
function Child({ name }) {
|
|
||||||
return <h1>{name}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child name={'hello world!!!'} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
hello world!!!
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support prop alias', ({ container }) => {
|
|
||||||
function Child({ name: alias }) {
|
|
||||||
return <h1>{alias}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child name={'prop alias'} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
prop alias
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support prop alias with default value', ({ container }) => {
|
|
||||||
function Child({ name: alias = 'default' }) {
|
|
||||||
return <h1>{alias}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
default
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('children', () => {
|
|
||||||
it('should support children', ({ container }) => {
|
|
||||||
function Child({ children }) {
|
|
||||||
return <h1>{children}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child>child content</Child>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
child content
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support children alias', ({ container }) => {
|
|
||||||
function Child({ children: alias }) {
|
|
||||||
return <h1>{alias}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child>children alias</Child>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
children alias
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: should support children default
|
|
||||||
it.fails('should support children alias with default value', ({ container }) => {
|
|
||||||
function Child({ children: alias = 'default child' }) {
|
|
||||||
return <h1>{alias}</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <Child />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
default child
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, expect, vi } from 'vitest';
|
|
||||||
import { domTest as it } from './utils';
|
|
||||||
import { render, View } from '../src';
|
|
||||||
|
|
||||||
describe('rendering', () => {
|
|
||||||
describe('basic', () => {
|
|
||||||
it('should support basic dom', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
return <h1>hello world!!!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
hello world!!!
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support text and variable mixing', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
const name = 'world';
|
|
||||||
return <h1>hello {name}!!!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
hello
|
|
||||||
world
|
|
||||||
!!!
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: SHOULD FIX
|
|
||||||
it.fails('should support dom has multiple layers ', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
Header
|
|
||||||
<h1>hello world!!!</h1>
|
|
||||||
<section>
|
|
||||||
<button>Add, Now is {count}</button>
|
|
||||||
</section>
|
|
||||||
Footer
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
hello world!!!
|
|
||||||
</h1>
|
|
||||||
<section>
|
|
||||||
<button>
|
|
||||||
Add, Now is
|
|
||||||
0
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: SHOULD FIX
|
|
||||||
it.fails('should support tag, text and variable mixing', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
let count = 'world';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
count: {count}
|
|
||||||
<button>Add, count is {count}</button>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('style', () => {
|
|
||||||
it('should apply styles correctly', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
return <h1 style={{ color: 'red' }}>hello world!!!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
const h1 = container.querySelector('h1');
|
|
||||||
expect(h1.style.color).toBe('red');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply multiple styles correctly', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
return <h1 style={{ color: 'red', fontSize: '20px' }}>hello world!!!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
const h1 = container.querySelector('h1');
|
|
||||||
expect(h1.style.color).toBe('red');
|
|
||||||
expect(h1.style.fontSize).toBe('20px');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should override styles correctly', ({ container }) => {
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<h1 style={{ color: 'red' }}>
|
|
||||||
<span style={{ color: 'blue' }}>hello world!!!</span>
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
const span = container.querySelector('span');
|
|
||||||
expect(span.style.color).toBe('blue');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle dynamic styles', ({ container }) => {
|
|
||||||
const color = 'red';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <h1 style={{ color }}>hello world!!!</h1>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
const h1 = container.querySelector('h1');
|
|
||||||
expect(h1.style.color).toBe('red');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('event', () => {
|
|
||||||
it('should handle click events', ({ container }) => {
|
|
||||||
const handleClick = vi.fn();
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return <button onClick={handleClick}>Click me</button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
const button = container.querySelector('button');
|
|
||||||
button.click();
|
|
||||||
|
|
||||||
expect(handleClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('components', () => {
|
|
||||||
it('should render components', ({ container }) => {
|
|
||||||
function Button({ children }) {
|
|
||||||
return <button>{children}</button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>hello world!!!</h1>
|
|
||||||
<Button>Click me</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(App, container);
|
|
||||||
expect(container).toMatchInlineSnapshot(`
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1>
|
|
||||||
hello world!!!
|
|
||||||
</h1>
|
|
||||||
<button>
|
|
||||||
Click me
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test } from 'vitest';
|
|
||||||
|
|
||||||
interface DomTestContext {
|
|
||||||
container: HTMLDivElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a new test type that extends the default test type and adds the container fixture.
|
|
||||||
export const domTest = test.extend<DomTestContext>({
|
|
||||||
container: async ({ task }, use) => {
|
|
||||||
const container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
await use(container);
|
|
||||||
container.remove();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "preserve",
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"esm": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// vitest.config.ts
|
|
||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
import inula from 'vite-plugin-inula-next';
|
|
||||||
import * as path from 'node:path';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
esbuild: {
|
|
||||||
jsx: 'preserve',
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@openinula/next': path.resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
conditions: ['dev'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// @ts-expect-error TODO: fix vite plugin interface is not compatible
|
|
||||||
inula(),
|
|
||||||
],
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom', // or 'jsdom', 'node'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -14,5 +14,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
|
presets: [
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{ targets: { node: 'current' }},
|
||||||
|
],
|
||||||
|
['@babel/preset-typescript'],
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,16 @@ import { babel } from '@rollup/plugin-babel';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: './index.ts',
|
input: './index.ts',
|
||||||
output: [
|
output: [{
|
||||||
{
|
|
||||||
file: 'dist/inulaRequest.js',
|
file: 'dist/inulaRequest.js',
|
||||||
format: 'umd',
|
format: 'umd',
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
name: 'inulaRequest',
|
name: 'inulaRequest',
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
file: 'dist/inulaRequest.esm-browser.js',
|
file: 'dist/inulaRequest.esm-browser.js',
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"name": "inula-router-config",
|
|
||||||
"module": "./esm/connectRouter.js",
|
"module": "./esm/connectRouter.js",
|
||||||
"main": "./cjs/connectRouter.js",
|
"main": "./cjs/connectRouter.js",
|
||||||
"types": "./@types/index.d.ts",
|
"types": "./@types/index.d.ts",
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,9 @@ export function getBaseHistory<S>(
|
||||||
Object.assign(historyProps, nextState);
|
Object.assign(historyProps, nextState);
|
||||||
}
|
}
|
||||||
historyProps.length = browserHistory.length;
|
historyProps.length = browserHistory.length;
|
||||||
const args = { location: historyProps.location, action: historyProps.action };
|
// 避免location饮用相同时setState不触发
|
||||||
|
const location = Object.assign({}, historyProps.location);
|
||||||
|
const args = { location: location, action: historyProps.action };
|
||||||
transitionManager.notifyListeners(args);
|
transitionManager.notifyListeners(args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,13 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
|
||||||
const pathDecoder = addHeadSlash;
|
const pathDecoder = addHeadSlash;
|
||||||
const pathEncoder = hashType === 'slash' ? addHeadSlash : stripHeadSlash;
|
const pathEncoder = hashType === 'slash' ? addHeadSlash : stripHeadSlash;
|
||||||
|
|
||||||
|
const startLocation = getHashContent(window.location.href);
|
||||||
|
const encodeLocation = pathEncoder(startLocation);
|
||||||
|
// 初始化hash格式不合法时会重定向
|
||||||
|
if (startLocation !== encodeLocation) {
|
||||||
|
window.location.replace(stripHash(window.location.href) + '#' + encodeLocation);
|
||||||
|
}
|
||||||
|
|
||||||
function getLocation() {
|
function getLocation() {
|
||||||
let hashPath = pathDecoder(getHashContent(window.location.hash));
|
let hashPath = pathDecoder(getHashContent(window.location.hash));
|
||||||
if (basename) {
|
if (basename) {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ describe('path lexer Test', () => {
|
||||||
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
|
expect(tokens).toStrictEqual([{ type: 'delimiter', value: '/' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("don't start with a slash", () => {
|
it('don\'t start with a slash', () => {
|
||||||
const func = () => lexer('abc.com');
|
const func = () => lexer('abc.com');
|
||||||
expect(func).toThrow(Error('Url must start with "/".'));
|
expect(func).toThrow(Error('Url must start with "/".'));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,13 @@ describe('parser test', () => {
|
||||||
it('url without end slash match wildcard', function () {
|
it('url without end slash match wildcard', function () {
|
||||||
const parser = createPathParser('/about/', { strictMode: false });
|
const parser = createPathParser('/about/', { strictMode: false });
|
||||||
const matched = parser.parse('/about');
|
const matched = parser.parse('/about');
|
||||||
expect(matched).toBeNull();
|
expect(matched).toStrictEqual({
|
||||||
|
path: '/about/',
|
||||||
|
url: '/about',
|
||||||
|
score: [10],
|
||||||
|
isExact: true,
|
||||||
|
param: {},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('url without end slash match wildcard (strictMode)', function () {
|
it('url without end slash match wildcard (strictMode)', function () {
|
||||||
|
|
@ -259,7 +265,7 @@ describe('parser test', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dynamic param with complex regexp pattern', () => {
|
it('dynamic param with complex regexp pattern', () => {
|
||||||
const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true });
|
const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true, caseSensitive: true });
|
||||||
const res = parser.parse('/detail/a123');
|
const res = parser.parse('/detail/a123');
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
isExact: true,
|
isExact: true,
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,14 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
|
||||||
const token = tokens[tokenIdx];
|
const token = tokens[tokenIdx];
|
||||||
const nextToken = tokens[tokenIdx + 1];
|
const nextToken = tokens[tokenIdx + 1];
|
||||||
switch (token.type) {
|
switch (token.type) {
|
||||||
case TokenType.Delimiter:
|
case TokenType.Delimiter: {
|
||||||
{
|
// 该分隔符后有可选参数则该分割符在匹配时是可选的
|
||||||
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
|
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
|
||||||
pattern += `/${hasOptional ? '?' : ''}`;
|
// 该分割符为最后一个且strictMode===false时,该分隔符在匹配时是可选的
|
||||||
}
|
const isSlashOptional = nextToken === undefined && !strictMode;
|
||||||
|
pattern += `/${hasOptional || isSlashOptional ? '?' : ''}`;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case TokenType.Static:
|
case TokenType.Static:
|
||||||
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
|
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
|
||||||
if (nextToken && nextToken.type === TokenType.Pattern) {
|
if (nextToken && nextToken.type === TokenType.Pattern) {
|
||||||
|
|
@ -112,8 +114,7 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
|
||||||
}
|
}
|
||||||
scores.push(MatchScore.static);
|
scores.push(MatchScore.static);
|
||||||
break;
|
break;
|
||||||
case TokenType.Param:
|
case TokenType.Param: {
|
||||||
{
|
|
||||||
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
|
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
|
||||||
let paramRegexp = '';
|
let paramRegexp = '';
|
||||||
if (nextToken) {
|
if (nextToken) {
|
||||||
|
|
@ -136,8 +137,8 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
|
||||||
pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`;
|
pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`;
|
||||||
keys.push(token.value);
|
keys.push(token.value);
|
||||||
scores.push(MatchScore.param);
|
scores.push(MatchScore.param);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case TokenType.WildCard:
|
case TokenType.WildCard:
|
||||||
keys.push(token.value);
|
keys.push(token.value);
|
||||||
pattern += `((?:${BASE_PARAM_PATTERN})${onlyHasWildCard ? '?' : ''}(?:/(?:${BASE_PARAM_PATTERN}))*)`;
|
pattern += `((?:${BASE_PARAM_PATTERN})${onlyHasWildCard ? '?' : ''}(?:/(?:${BASE_PARAM_PATTERN}))*)`;
|
||||||
|
|
@ -215,16 +216,15 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
|
||||||
}
|
}
|
||||||
path += params[token.value];
|
path += params[token.value];
|
||||||
break;
|
break;
|
||||||
case TokenType.WildCard:
|
case TokenType.WildCard: {
|
||||||
{
|
|
||||||
const wildCard = params['*'];
|
const wildCard = params['*'];
|
||||||
if (wildCard instanceof Array) {
|
if (wildCard instanceof Array) {
|
||||||
path += wildCard.join('/');
|
path += wildCard.join('/');
|
||||||
} else {
|
} else {
|
||||||
path += wildCard;
|
path += wildCard;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case TokenType.Delimiter:
|
case TokenType.Delimiter:
|
||||||
path += token.value;
|
path += token.value;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,16 @@ describe('Dom Attribute', () => {
|
||||||
Inula.render(<div {...emptyStringProps} />, container);
|
Inula.render(<div {...emptyStringProps} />, container);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dangerouslySetInnerHTML和children同时设置,只渲染children', () => {
|
||||||
|
Inula.act(() => {
|
||||||
|
Inula.render(
|
||||||
|
<div className="root" dangerouslySetInnerHTML={{ __html: '1234' }}>
|
||||||
|
123
|
||||||
|
</div>,
|
||||||
|
container
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(container.innerHTML).toBe('<div class="root">123</div>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -375,4 +375,93 @@ describe('Redux/React binding adapter', () => {
|
||||||
expect(getE(BUTTON).innerHTML).toBe('1');
|
expect(getE(BUTTON).innerHTML).toBe('1');
|
||||||
expect(getE(BUTTON2).innerHTML).toBe('true');
|
expect(getE(BUTTON2).innerHTML).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Nested use of connect', () => {
|
||||||
|
const updateInfo = [];
|
||||||
|
let dispatchMethod;
|
||||||
|
const ChildComponent = ({ childData, dispatch }) => {
|
||||||
|
const isMount = Inula.useRef(false);
|
||||||
|
|
||||||
|
Inula.useEffect(() => {
|
||||||
|
if (!isMount.current) {
|
||||||
|
isMount.current = true;
|
||||||
|
} else {
|
||||||
|
updateInfo.push('ChildComponent Updated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dispatchMethod = dispatch;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Child Component</h2>
|
||||||
|
<p id="child">{childData}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToPropsChild = state => ({ childData: state.childData });
|
||||||
|
|
||||||
|
const Child = connect(mapStateToPropsChild)(ChildComponent);
|
||||||
|
|
||||||
|
// Parent Component
|
||||||
|
const ParentComponent = ({ parentData }) => {
|
||||||
|
const isMount = Inula.useRef(false);
|
||||||
|
|
||||||
|
Inula.useEffect(() => {
|
||||||
|
if (!isMount.current) {
|
||||||
|
isMount.current = true;
|
||||||
|
} else {
|
||||||
|
updateInfo.push('ParentComponent Updated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Parent Component</h1>
|
||||||
|
<p id="parent">{parentData}</p>
|
||||||
|
<Child />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const mapStateToPropsParent = state => ({
|
||||||
|
parentData: state.parentData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Parent = connect(mapStateToPropsParent)(ParentComponent);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
parentData: 0,
|
||||||
|
childData: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INCREMENT_PARENT':
|
||||||
|
return { ...state, parentData: state.parentData + 1 };
|
||||||
|
case 'INCREMENT_CHILD':
|
||||||
|
return { ...state, childData: state.childData + 1 };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = createStore(reducer);
|
||||||
|
|
||||||
|
Inula.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<Parent />
|
||||||
|
</Provider>,
|
||||||
|
getE(CONTAINER)
|
||||||
|
);
|
||||||
|
expect(getE('child').innerHTML).toBe('0');
|
||||||
|
expect(getE('parent').innerHTML).toBe('0');
|
||||||
|
|
||||||
|
Inula.act(() => {
|
||||||
|
dispatchMethod({ type: 'INCREMENT_CHILD' });
|
||||||
|
});
|
||||||
|
expect(updateInfo).toStrictEqual(['ChildComponent Updated']);
|
||||||
|
|
||||||
|
expect(getE('child').innerHTML).toBe('1');
|
||||||
|
expect(getE('parent').innerHTML).toBe('0');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -109,16 +109,13 @@ function genConfig(mode) {
|
||||||
function genJSXRuntimeConfig(mode) {
|
function genJSXRuntimeConfig(mode) {
|
||||||
return {
|
return {
|
||||||
input: path.resolve(libDir, 'src', 'jsx-runtime.ts'),
|
input: path.resolve(libDir, 'src', 'jsx-runtime.ts'),
|
||||||
output: [
|
output: [{
|
||||||
{
|
|
||||||
file: outputResolve('jsx-runtime.js'),
|
file: outputResolve('jsx-runtime.js'),
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
file: outputResolve('jsx-runtime.esm-browser.js'),
|
file: outputResolve('jsx-runtime.esm-browser.js'),
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
plugins: [...getBasicPlugins(mode)],
|
plugins: [...getBasicPlugins(mode)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,25 @@ import { getPropDetails, PROPERTY_TYPE, PropDetails } from './PropertiesData';
|
||||||
|
|
||||||
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
|
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
|
||||||
|
|
||||||
|
const voidTagElements = [
|
||||||
|
'area',
|
||||||
|
'base',
|
||||||
|
'br',
|
||||||
|
'col',
|
||||||
|
'embed',
|
||||||
|
'hr',
|
||||||
|
'img',
|
||||||
|
'input',
|
||||||
|
'keygen',
|
||||||
|
'link',
|
||||||
|
'meta',
|
||||||
|
'param',
|
||||||
|
'source',
|
||||||
|
'track',
|
||||||
|
'wbr',
|
||||||
|
'menuitem',
|
||||||
|
];
|
||||||
|
|
||||||
// 是内置元素
|
// 是内置元素
|
||||||
export function isNativeElement(tagName: string, props: Record<string, any>) {
|
export function isNativeElement(tagName: string, props: Record<string, any>) {
|
||||||
return !tagName.includes('-') && props.is === undefined;
|
return !tagName.includes('-') && props.is === undefined;
|
||||||
|
|
@ -108,6 +127,18 @@ export function validateProps(type, props) {
|
||||||
throw new Error('style should be a object.');
|
throw new Error('style should be a object.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对于没有children的元素,设置dangerouslySetInnerHTML不生效
|
||||||
|
if (voidTagElements.includes(type)) {
|
||||||
|
if (props.dangerouslySetInnerHTML != null) {
|
||||||
|
delete props.dangerouslySetInnerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dangerouslySetInnerHTML和children同时设置,只渲染children
|
||||||
|
if (props.dangerouslySetInnerHTML != null && props.children != null) {
|
||||||
|
delete props.dangerouslySetInnerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// 校验属性
|
// 校验属性
|
||||||
const invalidProps = Object.keys(props).filter(key => !isValidProp(type, key, props[key]));
|
const invalidProps = Object.keys(props).filter(key => !isValidProp(type, key, props[key]));
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ import {
|
||||||
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
import { syncUpdates as flushSync } from './renderer/TreeBuilder';
|
||||||
import { toRaw } from './inulax/proxy/ProxyHandler';
|
import { toRaw } from './inulax/proxy/ProxyHandler';
|
||||||
|
|
||||||
|
const version = __VERSION__;
|
||||||
|
|
||||||
const Inula = {
|
const Inula = {
|
||||||
Children,
|
Children,
|
||||||
createRef,
|
createRef,
|
||||||
|
|
@ -122,9 +124,9 @@ const Inula = {
|
||||||
Profiler,
|
Profiler,
|
||||||
StrictMode,
|
StrictMode,
|
||||||
Suspense,
|
Suspense,
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const version = __VERSION__;
|
|
||||||
export {
|
export {
|
||||||
Children,
|
Children,
|
||||||
createRef,
|
createRef,
|
||||||
|
|
@ -178,6 +180,7 @@ export {
|
||||||
Profiler,
|
Profiler,
|
||||||
StrictMode,
|
StrictMode,
|
||||||
Suspense,
|
Suspense,
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ type ActionCreator = (...params: any[]) => ReduxAction;
|
||||||
type ActionCreators = { [key: string]: ActionCreator };
|
type ActionCreators = { [key: string]: ActionCreator };
|
||||||
export type BoundActionCreator = (...params: any[]) => void;
|
export type BoundActionCreator = (...params: any[]) => void;
|
||||||
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
||||||
type Dispatch = (action: ReduxAction) => any;
|
export type Dispatch = (action: ReduxAction) => any;
|
||||||
|
|
||||||
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
||||||
const boundActionCreators = {};
|
const boundActionCreators = {};
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,28 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* See the Mulan PSL v2 for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
import { useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from '../../renderer/hooks/HookExternal';
|
||||||
import { createContext } from '../../renderer/components/context/CreateContext';
|
import { createContext } from '../../renderer/components/context/CreateContext';
|
||||||
import { createElement } from '../../external/JSXElement';
|
import { createElement } from '../../external/JSXElement';
|
||||||
import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux';
|
import type { BoundActionCreator, ReduxAction, ReduxStoreHandler } from './redux';
|
||||||
import { forwardRef } from '../../renderer/components/ForwardRef';
|
import { forwardRef } from '../../renderer/components/ForwardRef';
|
||||||
|
import createSubscription, { Subscription } from './subscription';
|
||||||
|
import { ForwardRef } from '../../types';
|
||||||
|
import { isContextConsumer } from '../../external/InulaIs';
|
||||||
|
import { getSelector } from './reduxSelector';
|
||||||
|
|
||||||
const DefaultContext = createContext(null);
|
const DefaultContext = createContext<{ store: ReduxStoreHandler; subscription: Subscription }>(null as any);
|
||||||
type Context = typeof DefaultContext;
|
type Context = typeof DefaultContext;
|
||||||
|
type Selector = (state: unknown) => unknown;
|
||||||
|
|
||||||
|
type ConnectProps = {
|
||||||
|
store: ReduxStoreHandler;
|
||||||
|
context?: Context;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WrapperInnerProps = {
|
||||||
|
reduxAdapterRef?: ForwardRef<any>;
|
||||||
|
};
|
||||||
|
|
||||||
export function Provider({
|
export function Provider({
|
||||||
store,
|
store,
|
||||||
|
|
@ -28,32 +42,44 @@ export function Provider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
store: ReduxStoreHandler;
|
store: ReduxStoreHandler;
|
||||||
context: Context;
|
context?: Context;
|
||||||
children?: any[];
|
children?: any[];
|
||||||
}) {
|
}) {
|
||||||
|
const ctxValue = useMemo(() => {
|
||||||
|
const subscription = createSubscription(store);
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
subscription,
|
||||||
|
};
|
||||||
|
}, [store]);
|
||||||
|
const prevStoreValue = useMemo(() => store.getState(), [store]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const subscription = ctxValue.subscription;
|
||||||
|
subscription.stateChange = subscription.triggerNestedSubs;
|
||||||
|
subscription.trySubscribe();
|
||||||
|
if (prevStoreValue !== store.getState()) {
|
||||||
|
subscription.triggerNestedSubs();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
subscription.trySubscribe();
|
||||||
|
subscription.stateChange = undefined;
|
||||||
|
};
|
||||||
|
}, [ctxValue, prevStoreValue]);
|
||||||
|
|
||||||
const Context = context; // NOTE: bind redux API to inula API requires this renaming;
|
const Context = context; // NOTE: bind redux API to inula API requires this renaming;
|
||||||
return createElement(Context.Provider, { value: store }, children);
|
return createElement(Context.Provider, { value: ctxValue }, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStoreHook(context: Context): () => ReduxStoreHandler {
|
export function createStoreHook(context: Context): () => ReduxStoreHandler {
|
||||||
return () => {
|
return () => {
|
||||||
return useContext(context) as unknown as ReduxStoreHandler;
|
return useContext(context).store;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSelectorHook(context: Context): (selector?: ((state: unknown) => any) | undefined) => any {
|
export function createSelectorHook(context: Context) {
|
||||||
const store = createStoreHook(context)();
|
const store = createStoreHook(context)();
|
||||||
return function useSelector(selector = state => state) {
|
return function useSelector(selector: Selector = state => state) {
|
||||||
const [state, setState] = useState(() => store.getState());
|
return useSelectorWithStore(store, selector);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = store.subscribe(() => {
|
|
||||||
setState(store.getState());
|
|
||||||
});
|
|
||||||
return () => unsubscribe();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return selector(state);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +90,7 @@ export function createDispatchHook(context: Context): () => BoundActionCreator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelector = selector => {
|
export const useSelector = (selector: Selector) => {
|
||||||
return createSelectorHook(DefaultContext)(selector);
|
return createSelectorHook(DefaultContext)(selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -76,11 +102,11 @@ export const useStore = () => {
|
||||||
return createStoreHook(DefaultContext)();
|
return createStoreHook(DefaultContext)();
|
||||||
};
|
};
|
||||||
|
|
||||||
type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
|
export type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
|
||||||
type MapDispatchToPropsP<DispatchProps, OwnProps> =
|
export type MapDispatchToPropsP<DispatchProps, OwnProps> =
|
||||||
| { [key: string]: (...args: any[]) => ReduxAction }
|
| { [key: string]: (...args: any[]) => ReduxAction }
|
||||||
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
|
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
|
||||||
type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
export type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||||
stateProps: StateProps,
|
stateProps: StateProps,
|
||||||
dispatchProps: DispatchProps,
|
dispatchProps: DispatchProps,
|
||||||
ownProps: OwnProps
|
ownProps: OwnProps
|
||||||
|
|
@ -89,79 +115,131 @@ type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
||||||
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
|
||||||
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
|
||||||
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
|
||||||
type ConnectOption<State = any> = {
|
export type ConnectOption<State, StateProps, OwnProps> = {
|
||||||
areStatesEqual?: (oldState: State, newState: State) => boolean;
|
/** @deprecated */
|
||||||
context?: Context;
|
prue?: boolean;
|
||||||
forwardRef?: boolean;
|
forwardRef?: boolean;
|
||||||
|
context?: Context;
|
||||||
|
areOwnPropsEqual?: (newOwnProps: OwnProps, oldOwnProps: OwnProps) => boolean;
|
||||||
|
areStatePropsEqual?: (newStateProps: StateProps, oldStateProps: StateProps) => boolean;
|
||||||
|
areStatesEqual?: (oldState: State, newState: State) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({}) as StateProps,
|
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({}) as StateProps,
|
||||||
mapDispatchToProps: MapDispatchToPropsP<DispatchProps, OwnProps> = () => ({}) as DispatchProps,
|
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>,
|
||||||
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
|
mergeProps?: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>,
|
||||||
stateProps,
|
options: ConnectOption<any, StateProps, OwnProps> = {}
|
||||||
dispatchProps,
|
|
||||||
ownProps
|
|
||||||
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps }) as unknown as MergedProps,
|
|
||||||
options: ConnectOption = {}
|
|
||||||
): Connector<OwnProps, MergedProps> {
|
): Connector<OwnProps, MergedProps> {
|
||||||
//this component should bear the type returned from mapping functions
|
//this component should bear the type returned from mapping functions
|
||||||
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
|
|
||||||
const useStore = createStoreHook(options.context || DefaultContext);
|
|
||||||
|
|
||||||
|
const selectorOptions = {
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
mergeProps,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { context: storeContext = DefaultContext } = options;
|
||||||
|
|
||||||
|
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
|
||||||
//this component should mimic original type of component used
|
//this component should mimic original type of component used
|
||||||
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps & ConnectProps & WrapperInnerProps) => {
|
||||||
const store = useStore() as ReduxStoreHandler;
|
const [, forceUpdate] = useReducer(s => s + 1, 0);
|
||||||
const [state, setState] = useState(() => store.getState());
|
|
||||||
|
const propsFromContext = props.context;
|
||||||
|
const { reduxAdapterRef, ...wrappedProps } = props;
|
||||||
|
const usedContext = useMemo(() => {
|
||||||
|
return propsFromContext &&
|
||||||
|
propsFromContext.Consumer &&
|
||||||
|
isContextConsumer(createElement(propsFromContext.Consumer, {}))
|
||||||
|
? propsFromContext
|
||||||
|
: storeContext;
|
||||||
|
}, [propsFromContext, storeContext]);
|
||||||
|
|
||||||
|
const context = useContext(usedContext);
|
||||||
|
// 判断store是来自context还是props
|
||||||
|
const isStoreFromProps = !!props.store && !!props.store.getState && !!props.store.dispatch;
|
||||||
|
|
||||||
|
const store = isStoreFromProps ? props.store : context.store;
|
||||||
|
|
||||||
|
const [subscription, triggerNestedSubs] = useMemo(() => {
|
||||||
|
const subscription = createSubscription(store, isStoreFromProps ? null : context.subscription);
|
||||||
|
const triggerNestedSubs = subscription.triggerNestedSubs.bind(subscription);
|
||||||
|
return [subscription, triggerNestedSubs];
|
||||||
|
}, [store, isStoreFromProps, context]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果在调用listener中间组件被卸载,subscription会变为空
|
||||||
|
* 在一开始就复制一份triggerNestedSubs保证即使组件卸载也可以正常使用
|
||||||
|
*/
|
||||||
|
|
||||||
|
const overrideContext = useMemo(
|
||||||
|
() => (isStoreFromProps ? context : { ...context, subscription }),
|
||||||
|
[isStoreFromProps, context, subscription]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用Ref存储最新的子组件Props,在更新时进行比较,防止重复渲染
|
||||||
|
const latestChildProps = useRef<any>();
|
||||||
|
const latestWrappedProps = useRef(wrappedProps);
|
||||||
|
const childPropsFormStore = useRef<any>();
|
||||||
|
const isRendering = useRef<boolean>(false);
|
||||||
|
|
||||||
|
const selector = useMemo(() => getSelector(store, selectorOptions), [store]);
|
||||||
|
|
||||||
|
const childProps = useMemo(() => {
|
||||||
|
return childPropsFormStore.current && wrappedProps === latestChildProps.current
|
||||||
|
? childPropsFormStore.current
|
||||||
|
: selector(store.getState(), wrappedProps as OwnProps);
|
||||||
|
}, [store, wrappedProps, latestWrappedProps]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = store.subscribe(() => {
|
latestChildProps.current = childProps;
|
||||||
setState(store.getState());
|
latestWrappedProps.current = wrappedProps;
|
||||||
});
|
isRendering.current = false;
|
||||||
return () => unsubscribe();
|
if (childPropsFormStore.current) {
|
||||||
}, []);
|
childPropsFormStore.current = null;
|
||||||
|
triggerNestedSubs();
|
||||||
const previous = useRef<{ state: { [key: string]: any }; mappedState: StateProps }>({
|
}
|
||||||
state: {},
|
|
||||||
mappedState: {} as StateProps,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mappedState: StateProps;
|
useEffect(() => {
|
||||||
if (options.areStatesEqual) {
|
let isUnsubscribe = false;
|
||||||
if (options.areStatesEqual(previous.current.state, state)) {
|
|
||||||
mappedState = previous.current.mappedState as StateProps;
|
const update = () => {
|
||||||
} else {
|
if (isUnsubscribe) {
|
||||||
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
return;
|
||||||
previous.current.mappedState = mappedState;
|
}
|
||||||
|
const latestStoreState = store.getState();
|
||||||
|
const newChildProps = selector(latestStoreState, latestWrappedProps.current as OwnProps);
|
||||||
|
// 如果新的子组件的props和之前不同,就更新ref对象的值,并强制更新组件
|
||||||
|
if (newChildProps === latestChildProps.current) {
|
||||||
|
if (!isRendering.current) {
|
||||||
|
triggerNestedSubs();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mappedState = mapStateToProps ? mapStateToProps(state, props) : ({} as StateProps);
|
latestChildProps.current = newChildProps;
|
||||||
previous.current.mappedState = mappedState;
|
childPropsFormStore.current = newChildProps;
|
||||||
|
isRendering.current = true;
|
||||||
|
forceUpdate();
|
||||||
}
|
}
|
||||||
let mappedDispatch: DispatchProps = {} as DispatchProps;
|
|
||||||
if (mapDispatchToProps) {
|
|
||||||
if (typeof mapDispatchToProps === 'object') {
|
|
||||||
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
|
||||||
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
|
||||||
store.dispatch(value(...args));
|
|
||||||
setState(store.getState());
|
|
||||||
};
|
};
|
||||||
});
|
// 订阅store的变化
|
||||||
} else {
|
subscription.stateChange = update;
|
||||||
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
subscription.trySubscribe();
|
||||||
}
|
update();
|
||||||
}
|
return () => {
|
||||||
mappedDispatch = Object.assign({}, mappedDispatch, { dispatch: store.dispatch });
|
isUnsubscribe = true;
|
||||||
const mergedProps = (
|
subscription.trySubscribe();
|
||||||
mergeProps ||
|
subscription.stateChange = undefined;
|
||||||
((state, dispatch, originalProps) => {
|
};
|
||||||
return { ...state, ...dispatch, ...originalProps };
|
}, [store, subscription, selector]);
|
||||||
})
|
|
||||||
)(mappedState, mappedDispatch, props);
|
|
||||||
|
|
||||||
previous.current.state = state;
|
const renderComponent = useMemo(() => {
|
||||||
|
return createElement(Component, { ...childProps, ref: reduxAdapterRef });
|
||||||
|
}, [Component, childProps, reduxAdapterRef]);
|
||||||
|
|
||||||
return createElement(Component, mergedProps);
|
return createElement(usedContext.Provider, { value: overrideContext }, renderComponent);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.forwardRef) {
|
if (options.forwardRef) {
|
||||||
|
|
@ -174,3 +252,57 @@ export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
return Wrapper;
|
return Wrapper;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useSelectorWithStore(store: ReduxStoreHandler, selector: Selector) {
|
||||||
|
const [, forceUpdate] = useReducer(s => s + 1, 0);
|
||||||
|
|
||||||
|
const latestSelector = useRef<(state: any) => unknown>();
|
||||||
|
const latestState = useRef<any>();
|
||||||
|
const latestSelectedState = useRef<any>();
|
||||||
|
|
||||||
|
const state = store.getState();
|
||||||
|
let selectedState: any;
|
||||||
|
|
||||||
|
// 检查选择器或状态自上次渲染以来是否发生了变更
|
||||||
|
if (selector !== latestSelector.current || state !== latestState.current) {
|
||||||
|
const newSelectedState = selector(state);
|
||||||
|
if (latestSelectedState.current === undefined || newSelectedState !== latestSelectedState.current) {
|
||||||
|
selectedState = newSelectedState;
|
||||||
|
} else {
|
||||||
|
selectedState = latestSelectedState.current;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedState = latestSelectedState.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
latestSelector.current = selector;
|
||||||
|
latestState.current = state;
|
||||||
|
latestSelectedState.current = selectedState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 订阅存储并在状态变更时更新组件
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const update = () => {
|
||||||
|
const newState = store.getState();
|
||||||
|
if (newState === latestState.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSelectedState = latestSelector.current!(newState);
|
||||||
|
if (newSelectedState === latestSelectedState.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
latestSelectedState.current = newSelectedState;
|
||||||
|
latestState.current = newState;
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
const unsubscribe = store.subscribe(() => update());
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
|
return selectedState;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isSame, shallowCompare } from '../../renderer/utils/compare';
|
||||||
|
import type { Dispatch, ReduxAction, ReduxStoreHandler } from './redux';
|
||||||
|
import type { MapStateToPropsP, MapDispatchToPropsP, MergePropsP, ConnectOption } from './reduxReact';
|
||||||
|
|
||||||
|
type StateOrDispatch<S extends ReduxAction = ReduxAction> = S | Dispatch;
|
||||||
|
|
||||||
|
const defaultMerge = (...args: any[]) => {
|
||||||
|
return Object.assign({}, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReduxSelector<OwnProps = unknown> {
|
||||||
|
dependsOnOwnProps: boolean;
|
||||||
|
|
||||||
|
(stateOrDispatch?: StateOrDispatch, ownProps?: OwnProps): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDependsOnOwnProps = (propsMapping: any) => {
|
||||||
|
return propsMapping.dependsOnOwnProps ? Boolean(propsMapping.dependsOnOwnProps) : propsMapping.length !== 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleMapToProps<StateProps, OwnProps>(
|
||||||
|
mapStateToProps?: MapStateToPropsP<StateProps, OwnProps>
|
||||||
|
): ReduxSelector {
|
||||||
|
if (typeof mapStateToProps === 'function') {
|
||||||
|
const proxy = <
|
||||||
|
ReduxSelector & {
|
||||||
|
mapToProps: any;
|
||||||
|
}
|
||||||
|
>function mapToPropsProxy(stateOrDispatch: StateOrDispatch, ownProps?: any) {
|
||||||
|
return proxy.dependsOnOwnProps ? proxy.mapToProps(stateOrDispatch, ownProps) : proxy.mapToProps(stateOrDispatch);
|
||||||
|
};
|
||||||
|
proxy.dependsOnOwnProps = true;
|
||||||
|
|
||||||
|
proxy.mapToProps = function (stateOrDispatch: StateOrDispatch, ownProps: any) {
|
||||||
|
proxy.mapToProps = mapStateToProps;
|
||||||
|
proxy.dependsOnOwnProps = isDependsOnOwnProps(mapStateToProps);
|
||||||
|
let props = proxy(stateOrDispatch, ownProps);
|
||||||
|
|
||||||
|
if (typeof props === 'function') {
|
||||||
|
props.mapToProps = props;
|
||||||
|
proxy.dependsOnOwnProps = isDependsOnOwnProps(props);
|
||||||
|
props = proxy(stateOrDispatch, ownProps);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
return proxy;
|
||||||
|
} else {
|
||||||
|
const selector = () => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
selector.dependsOnOwnProps = false;
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMapDispatchToProps<DispatchProps, OwnProps>(
|
||||||
|
dispatch: Dispatch,
|
||||||
|
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>
|
||||||
|
): ReduxSelector {
|
||||||
|
if (!mapDispatchToProps) {
|
||||||
|
const selector = () => {
|
||||||
|
return { dispatch: dispatch };
|
||||||
|
};
|
||||||
|
selector.dependsOnOwnProps = false;
|
||||||
|
return selector;
|
||||||
|
} else if (typeof mapDispatchToProps === 'function') {
|
||||||
|
return handleMapToProps(mapDispatchToProps);
|
||||||
|
} else {
|
||||||
|
const selector = () => {
|
||||||
|
const mappedDispatch = {};
|
||||||
|
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||||
|
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
||||||
|
dispatch(value(...args));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return mappedDispatch;
|
||||||
|
};
|
||||||
|
selector.dependsOnOwnProps = false;
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelector<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
|
store: ReduxStoreHandler,
|
||||||
|
{
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
mergeProps,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
mapStateToProps: MapStateToPropsP<StateProps, OwnProps>;
|
||||||
|
mapDispatchToProps?: MapDispatchToPropsP<DispatchProps, OwnProps>;
|
||||||
|
mergeProps?: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>;
|
||||||
|
options: ConnectOption<unknown, StateProps, OwnProps>;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { dispatch } = store;
|
||||||
|
const mappedStateToProps = handleMapToProps<StateProps, OwnProps>(mapStateToProps);
|
||||||
|
const mappedDispatchToProps = handleMapDispatchToProps<DispatchProps, OwnProps>(dispatch, mapDispatchToProps);
|
||||||
|
const mergeMethod = mergeProps || defaultMerge;
|
||||||
|
|
||||||
|
return pureSelectorCreator(mappedStateToProps, mappedDispatchToProps, mergeMethod, dispatch, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pureSelectorCreator<StateProps, DispatchProps, OwnProps, MergedProps>(
|
||||||
|
mapStateToProps: ReduxSelector,
|
||||||
|
mapDispatchToProps: ReduxSelector,
|
||||||
|
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps>,
|
||||||
|
dispatch: Dispatch,
|
||||||
|
options: ConnectOption<unknown, StateProps, OwnProps>
|
||||||
|
) {
|
||||||
|
let hasRun = false;
|
||||||
|
let state: any;
|
||||||
|
let ownProps: OwnProps;
|
||||||
|
let stateProps: StateProps;
|
||||||
|
let dispatchProps: DispatchProps;
|
||||||
|
let mergedProps: MergedProps;
|
||||||
|
|
||||||
|
const { areStatesEqual = isSame, areOwnPropsEqual = shallowCompare, areStatePropsEqual = shallowCompare } = options;
|
||||||
|
|
||||||
|
// 首次运行该函数
|
||||||
|
function firstRun(initState: any, initOwnProps: OwnProps) {
|
||||||
|
state = initState;
|
||||||
|
ownProps = initOwnProps;
|
||||||
|
stateProps = mapStateToProps(state, ownProps);
|
||||||
|
dispatchProps = mapDispatchToProps(dispatch, ownProps);
|
||||||
|
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
|
||||||
|
hasRun = true;
|
||||||
|
return mergedProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicateRun(newState: any, newOwnProps: OwnProps) {
|
||||||
|
const isStateChange = !areStatesEqual(newState, state);
|
||||||
|
const isPropsChange = !areOwnPropsEqual(newOwnProps, ownProps);
|
||||||
|
state = newState;
|
||||||
|
ownProps = newOwnProps;
|
||||||
|
if (isStateChange && isPropsChange) {
|
||||||
|
stateProps = mapStateToProps(state, ownProps);
|
||||||
|
if (mapDispatchToProps.dependsOnOwnProps) {
|
||||||
|
dispatchProps = mapDispatchToProps(dispatch, ownProps);
|
||||||
|
}
|
||||||
|
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
|
||||||
|
return mergedProps;
|
||||||
|
}
|
||||||
|
if (isPropsChange) {
|
||||||
|
if (mapStateToProps.dependsOnOwnProps) {
|
||||||
|
stateProps = mapStateToProps(state, ownProps);
|
||||||
|
}
|
||||||
|
if (mapDispatchToProps.dependsOnOwnProps) {
|
||||||
|
dispatchProps = mapDispatchToProps(dispatch, ownProps);
|
||||||
|
}
|
||||||
|
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
|
||||||
|
return mergedProps;
|
||||||
|
}
|
||||||
|
if (isStateChange) {
|
||||||
|
const latestStateProps = mapStateToProps(state, ownProps);
|
||||||
|
const isStatePropsChange = !areStatePropsEqual(latestStateProps, stateProps);
|
||||||
|
stateProps = latestStateProps;
|
||||||
|
if (isStatePropsChange) {
|
||||||
|
mergedProps = mergeProps(stateProps, dispatchProps, ownProps);
|
||||||
|
}
|
||||||
|
return mergedProps;
|
||||||
|
}
|
||||||
|
return mergedProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (newState: any, newOwnProps: OwnProps) {
|
||||||
|
return hasRun ? duplicateRun(newState, newOwnProps) : firstRun(newState, newOwnProps);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getSelector };
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { unstable_batchedUpdates } from '../../dom/DOMExternal';
|
||||||
|
import { ReduxStoreHandler } from './redux';
|
||||||
|
|
||||||
|
type LinkListNode<T> = {
|
||||||
|
next: LinkListNode<T>;
|
||||||
|
prev: LinkListNode<T>;
|
||||||
|
value: T;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type CallBack = () => void;
|
||||||
|
|
||||||
|
interface ListenerManager {
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
trigger(): void;
|
||||||
|
|
||||||
|
subscribe(cb: CallBack): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function batchUpdate(callback: () => any) {
|
||||||
|
unstable_batchedUpdates(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinkedList<T>() {
|
||||||
|
let firstNode: LinkListNode<T> = null;
|
||||||
|
let lastNode: LinkListNode<T> = null;
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
firstNode = null;
|
||||||
|
lastNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIterator(): T[] {
|
||||||
|
const data: T[] = [];
|
||||||
|
let curNode = firstNode;
|
||||||
|
while (curNode) {
|
||||||
|
data.push(curNode.value);
|
||||||
|
curNode = curNode.next;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(element: T): NonNullable<LinkListNode<T>> {
|
||||||
|
let newNode: LinkListNode<T>;
|
||||||
|
if (!firstNode || !lastNode) {
|
||||||
|
newNode = {
|
||||||
|
value: element,
|
||||||
|
prev: null,
|
||||||
|
next: null,
|
||||||
|
};
|
||||||
|
firstNode = lastNode = newNode;
|
||||||
|
return newNode;
|
||||||
|
} else {
|
||||||
|
newNode = {
|
||||||
|
value: element,
|
||||||
|
prev: lastNode,
|
||||||
|
next: null,
|
||||||
|
};
|
||||||
|
lastNode.next = newNode;
|
||||||
|
lastNode = newNode;
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNode(node: NonNullable<LinkListNode<T>>) {
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
} else {
|
||||||
|
lastNode = node.prev;
|
||||||
|
}
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
} else {
|
||||||
|
firstNode = node.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
clear,
|
||||||
|
removeNode,
|
||||||
|
getIterator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListenerManager(): ListenerManager {
|
||||||
|
const linkedList = getLinkedList<CallBack>();
|
||||||
|
|
||||||
|
function subscribe(cb: CallBack): () => void {
|
||||||
|
const listener = linkedList.add(cb);
|
||||||
|
return () => linkedList.removeNode(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trigger() {
|
||||||
|
const listeners = linkedList.getIterator();
|
||||||
|
batchUpdate(() => {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
linkedList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clear,
|
||||||
|
trigger,
|
||||||
|
subscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subscription {
|
||||||
|
stateChange?: () => any;
|
||||||
|
|
||||||
|
addNestedSub(listener: CallBack): CallBack;
|
||||||
|
|
||||||
|
triggerNestedSubs(): void;
|
||||||
|
|
||||||
|
trySubscribe(): void;
|
||||||
|
|
||||||
|
tryUnsubscribe(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nullListenerStore = {} as unknown as ListenerManager;
|
||||||
|
|
||||||
|
function createSubscription(store: ReduxStoreHandler, parentSub: Subscription | null = null): Subscription {
|
||||||
|
let unsubscribe: CallBack | undefined;
|
||||||
|
let listenerStore: ListenerManager = nullListenerStore;
|
||||||
|
|
||||||
|
function addNestedSub(listener: CallBack) {
|
||||||
|
trySubscribe();
|
||||||
|
return listenerStore.subscribe(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerNestedSubs() {
|
||||||
|
listenerStore.trigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeChangeHandler() {
|
||||||
|
if (typeof subscription.stateChange === 'function') {
|
||||||
|
subscription.stateChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trySubscribe() {
|
||||||
|
if (!unsubscribe) {
|
||||||
|
unsubscribe = parentSub ? parentSub.addNestedSub(storeChangeHandler) : store.subscribe(storeChangeHandler);
|
||||||
|
listenerStore = getListenerManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryUnsubscribe() {
|
||||||
|
if (typeof unsubscribe === 'function') {
|
||||||
|
unsubscribe();
|
||||||
|
unsubscribe = undefined;
|
||||||
|
listenerStore.clear();
|
||||||
|
listenerStore = nullListenerStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription: Subscription = {
|
||||||
|
stateChange: undefined,
|
||||||
|
addNestedSub,
|
||||||
|
triggerNestedSubs,
|
||||||
|
trySubscribe,
|
||||||
|
tryUnsubscribe,
|
||||||
|
};
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createSubscription;
|
||||||
|
|
@ -117,7 +117,7 @@ export class Observer implements IObserver {
|
||||||
vNodes: nodesList.map((vNode: VNode) => {
|
vNodes: nodesList.map((vNode: VNode) => {
|
||||||
let realNode = vNode.realNode;
|
let realNode = vNode.realNode;
|
||||||
let searchedNode: VNode | null | undefined = vNode;
|
let searchedNode: VNode | null | undefined = vNode;
|
||||||
while (!realNode) {
|
while (searchedNode && !realNode) {
|
||||||
searchedNode = searchedNode?.child;
|
searchedNode = searchedNode?.child;
|
||||||
realNode = searchedNode?.realNode;
|
realNode = searchedNode?.realNode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,7 @@ export interface IObserver {
|
||||||
clearByVNode: (vNode: any) => void;
|
clearByVNode: (vNode: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StoreConfig<
|
export type StoreConfig<S extends Record<string, unknown>, A extends UserActions<S>, C extends UserComputedValues<S>> = {
|
||||||
S extends Record<string, unknown>,
|
|
||||||
A extends UserActions<S>,
|
|
||||||
C extends UserComputedValues<S>,
|
|
||||||
> = {
|
|
||||||
id?: string;
|
id?: string;
|
||||||
state?: S;
|
state?: S;
|
||||||
actions?: A;
|
actions?: A;
|
||||||
|
|
@ -49,11 +45,7 @@ export type UserActions<S extends Record<string, unknown>> = {
|
||||||
[K: string]: ActionFunction<S>;
|
[K: string]: ActionFunction<S>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ActionFunction<S extends Record<string, unknown>> = (
|
export type ActionFunction<S extends Record<string, unknown>> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
|
||||||
this: StoreObj<S, any, any>,
|
|
||||||
state: S,
|
|
||||||
...args: any[]
|
|
||||||
) => any;
|
|
||||||
|
|
||||||
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
export type StoreActions<S extends Record<string, unknown>, A extends UserActions<S>> = {
|
||||||
[K in keyof A]: Action<A[K], S>;
|
[K in keyof A]: Action<A[K], S>;
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,10 @@ function getChildByIndex(vNode: VNode, idx: number) {
|
||||||
// 从多个更新节点中,计算出开始节点。即:找到最近的共同的父辈节点
|
// 从多个更新节点中,计算出开始节点。即:找到最近的共同的父辈节点
|
||||||
export function calcStartUpdateVNode(treeRoot: VNode) {
|
export function calcStartUpdateVNode(treeRoot: VNode) {
|
||||||
const toUpdateNodes = Array.from(treeRoot.toUpdateNodes!);
|
const toUpdateNodes = Array.from(treeRoot.toUpdateNodes!);
|
||||||
|
// 所有待更新元素的parent为null说明该node的父元素已经被卸载,应该从根节点发起更新
|
||||||
|
if (toUpdateNodes.every(node => node.parent === null)) {
|
||||||
|
return treeRoot;
|
||||||
|
}
|
||||||
|
|
||||||
if (toUpdateNodes.length === 0) {
|
if (toUpdateNodes.length === 0) {
|
||||||
return treeRoot;
|
return treeRoot;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,12 @@ export function captureRender(processing: VNode): VNode | null {
|
||||||
processUpdates(processing, inst, nextProps);
|
processUpdates(processing, inst, nextProps);
|
||||||
|
|
||||||
// 如果 props, state, context 都没有变化且 isForceUpdate 为 false则不需要更新
|
// 如果 props, state, context 都没有变化且 isForceUpdate 为 false则不需要更新
|
||||||
shouldUpdate = oldProps !== processing.props || inst.state !== processing.state || processing.isForceUpdate;
|
shouldUpdate =
|
||||||
|
oldProps !== processing.props ||
|
||||||
|
inst.state !== processing.state ||
|
||||||
|
processing.isForceUpdate ||
|
||||||
|
// 响应式状态管理器中的值变化,需要更新
|
||||||
|
processing.isStoreChange;
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
// derivedStateFromProps会修改nextState,因此需要调用
|
// derivedStateFromProps会修改nextState,因此需要调用
|
||||||
|
|
@ -167,7 +172,7 @@ export function captureRender(processing: VNode): VNode | null {
|
||||||
}
|
}
|
||||||
// 如果捕获了 error,必须更新
|
// 如果捕获了 error,必须更新
|
||||||
const isCatchError = (processing.flags & DidCapture) === DidCapture;
|
const isCatchError = (processing.flags & DidCapture) === DidCapture;
|
||||||
shouldUpdate = isCatchError || shouldUpdate || processing.isStoreChange;
|
shouldUpdate = isCatchError || shouldUpdate;
|
||||||
|
|
||||||
// 更新ref
|
// 更新ref
|
||||||
markRef(processing);
|
markRef(processing);
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export interface ChildrenType {
|
||||||
toArray(children: InulaNode | InulaNode[]): Array<Exclude<InulaNode, boolean | null | undefined>>;
|
toArray(children: InulaNode | InulaNode[]): Array<Exclude<InulaNode, boolean | null | undefined>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRef<T> = ((inst: T | null) => void) | MutableRef<T | null> | null;
|
export type ForwardRef<T> = ((inst: T | null) => void) | MutableRef<T | null> | null;
|
||||||
|
|
||||||
export interface ForwardRefRenderFunc<T, P = KVObject> {
|
export interface ForwardRefRenderFunc<T, P = KVObject> {
|
||||||
(props: P, ref: ForwardRef<T>): InulaElement | null;
|
(props: P, ref: ForwardRef<T>): InulaElement | null;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# @openinlua/babel-api
|
|
||||||
A package that encapsulates the babel API for use in the transpiler.
|
|
||||||
|
|
||||||
To implement the dependency injection pattern, the package exports a function that registers the babel API in the
|
|
||||||
transpiler.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { registerBabelAPI } from '@openinlua/babel-api';
|
|
||||||
|
|
||||||
function plugin(api: typeof babel) {
|
|
||||||
registerBabelAPI(api);
|
|
||||||
|
|
||||||
// Your babel plugin code here.
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And then you can import to use it.
|
|
||||||
> types can use as a `type` or as a `namespace` for the babel API.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { types as t } from '@openinlua/babel-api';
|
|
||||||
|
|
||||||
t.isIdentifier(node as t.Node);
|
|
||||||
```
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@openinula/babel-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"type": "module",
|
|
||||||
"main": "src/index.mjs",
|
|
||||||
"typings": "src/index.d.ts",
|
|
||||||
"files": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.23.3",
|
|
||||||
"@babel/types": "^7.24.0",
|
|
||||||
"@types/babel__core": "^7.20.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import type babel from '@babel/core';
|
|
||||||
|
|
||||||
// use .d.ts to satisfy the type check
|
|
||||||
export * as types from '@babel/types';
|
|
||||||
|
|
||||||
export declare function register(api: typeof babel): void;
|
|
||||||
export declare function getBabelApi(): typeof babel;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
/** @type {null | typeof import('@babel/core').types} */
|
|
||||||
let _t = null;
|
|
||||||
/** @type {null | typeof import('@babel/core')} */
|
|
||||||
let babelApi = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@babel/core')} api
|
|
||||||
*/
|
|
||||||
export const register = api => {
|
|
||||||
babelApi = api;
|
|
||||||
_t = api.types;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {typeof import('@babel/core')}
|
|
||||||
*/
|
|
||||||
export const getBabelApi = () => {
|
|
||||||
if (!babelApi) {
|
|
||||||
throw new Error('Please call register() before using the babel api');
|
|
||||||
}
|
|
||||||
return babelApi;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const types = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get: (_, p, receiver) => {
|
|
||||||
if (!_t) {
|
|
||||||
throw new Error('Please call register() before using the babel types');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p in _t) {
|
|
||||||
return Reflect.get(_t, p, receiver);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
# babel-preset-inula-next
|
|
||||||
|
|
||||||
## 0.0.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies
|
|
||||||
- @openinula/class-transformer@0.0.2
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- 2f9d373: feat: change babel import
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
{
|
|
||||||
"name": "babel-preset-inula-next",
|
|
||||||
"version": "0.0.3",
|
|
||||||
"author": {
|
|
||||||
"name": "IanDx",
|
|
||||||
"email": "iandxssxx@gmail.com"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"dlight.js",
|
|
||||||
"babel-preset"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"typings": "dist/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsup --sourcemap",
|
|
||||||
"test": "vitest"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"@types/node": "^20.10.5",
|
|
||||||
"tsup": "^6.7.0",
|
|
||||||
"typescript": "^5.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.23.3",
|
|
||||||
"@babel/generator": "^7.23.6",
|
|
||||||
"@babel/parser": "^7.24.4",
|
|
||||||
"@babel/plugin-syntax-decorators": "^7.23.3",
|
|
||||||
"@babel/plugin-syntax-jsx": "7.16.7",
|
|
||||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
|
||||||
"@babel/traverse": "^7.24.1",
|
|
||||||
"@babel/types": "^7.24.0",
|
|
||||||
"@openinula/class-transformer": "workspace:*",
|
|
||||||
"@openinula/reactivity-parser": "workspace:*",
|
|
||||||
"@openinula/view-generator": "workspace:*",
|
|
||||||
"@openinula/view-parser": "workspace:*",
|
|
||||||
"@types/babel-types": "^7.0.15",
|
|
||||||
"@types/babel__generator": "^7.6.8",
|
|
||||||
"@types/babel__parser": "^7.1.1",
|
|
||||||
"@types/babel__traverse": "^7.6.8",
|
|
||||||
"@openinula/jsx-view-parser": "workspace:*",
|
|
||||||
"@openinula/babel-api": "workspace:*",
|
|
||||||
"minimatch": "^9.0.3",
|
|
||||||
"vitest": "^1.4.0"
|
|
||||||
},
|
|
||||||
"tsup": {
|
|
||||||
"entry": [
|
|
||||||
"src/index.ts"
|
|
||||||
],
|
|
||||||
"format": [
|
|
||||||
"cjs",
|
|
||||||
"esm"
|
|
||||||
],
|
|
||||||
"clean": true,
|
|
||||||
"dts": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath } from '@babel/core';
|
|
||||||
import { LifeCycle, Visitor } from './types';
|
|
||||||
import { addLifecycle, addWatch } from './nodeFactory';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
|
||||||
import { extractFnFromMacro, getFnBody } from '../utils';
|
|
||||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
|
||||||
|
|
||||||
function isLifeCycleName(name: string): name is LifeCycle {
|
|
||||||
return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Analyze the functional macro in the function component
|
|
||||||
* 1. lifecycle
|
|
||||||
* 1. willMount
|
|
||||||
* 2. onMount
|
|
||||||
* 3. willUnMount
|
|
||||||
* 4. onUnmount
|
|
||||||
* 2. watch
|
|
||||||
*/
|
|
||||||
export function functionalMacroAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
ExpressionStatement(path: NodePath<t.ExpressionStatement>, ctx) {
|
|
||||||
const expression = path.get('expression');
|
|
||||||
if (expression.isCallExpression()) {
|
|
||||||
const callee = expression.get('callee');
|
|
||||||
if (callee.isIdentifier()) {
|
|
||||||
const calleeName = callee.node.name;
|
|
||||||
// lifecycle
|
|
||||||
if (isLifeCycleName(calleeName)) {
|
|
||||||
const fnNode = extractFnFromMacro(expression, calleeName);
|
|
||||||
addLifecycle(ctx.current, calleeName, getFnBody(fnNode).node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch
|
|
||||||
if (calleeName === WATCH) {
|
|
||||||
const fnNode = extractFnFromMacro(expression, WATCH);
|
|
||||||
const deps = getWatchDeps(expression);
|
|
||||||
|
|
||||||
const depMask = getDependenciesFromNode(deps ?? fnNode, ctx);
|
|
||||||
|
|
||||||
addWatch(ctx.current, fnNode, depMask);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.unhandledNode.push(path.node);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWatchDeps(callExpression: NodePath<t.CallExpression>) {
|
|
||||||
const args = callExpression.get('arguments');
|
|
||||||
if (!args[1]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deps: null | NodePath<t.ArrayExpression> = null;
|
|
||||||
if (args[1].isArrayExpression()) {
|
|
||||||
deps = args[1];
|
|
||||||
} else {
|
|
||||||
console.error('watch deps should be an array expression');
|
|
||||||
}
|
|
||||||
return deps;
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import { type NodePath } from '@babel/core';
|
|
||||||
import { propsAnalyze } from './propsAnalyze';
|
|
||||||
import { AnalyzeContext, Analyzer, ComponentNode, Visitor } from './types';
|
|
||||||
import { addLifecycle, createComponentNode } from './nodeFactory';
|
|
||||||
import { variablesAnalyze } from './variablesAnalyze';
|
|
||||||
import { functionalMacroAnalyze } from './functionalMacroAnalyze';
|
|
||||||
import { getFnBody } from '../utils';
|
|
||||||
import { viewAnalyze } from './viewAnalyze';
|
|
||||||
import { WILL_MOUNT } from '../constants';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
const builtinAnalyzers = [propsAnalyze, variablesAnalyze, functionalMacroAnalyze, viewAnalyze];
|
|
||||||
|
|
||||||
function mergeVisitor(...visitors: Analyzer[]): Visitor {
|
|
||||||
return visitors.reduce<Visitor<AnalyzeContext>>((acc, cur) => {
|
|
||||||
return { ...acc, ...cur() };
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// walk through the function component body
|
|
||||||
export function analyzeFnComp(
|
|
||||||
fnNode: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
|
||||||
componentNode: ComponentNode,
|
|
||||||
{ htmlTags, analyzers }: { analyzers: Analyzer[]; htmlTags: string[] },
|
|
||||||
level = 0
|
|
||||||
) {
|
|
||||||
const visitor = mergeVisitor(...analyzers);
|
|
||||||
const context: AnalyzeContext = {
|
|
||||||
level,
|
|
||||||
current: componentNode,
|
|
||||||
htmlTags,
|
|
||||||
analyzers,
|
|
||||||
unhandledNode: [],
|
|
||||||
traverse: (path: NodePath<t.Statement>, ctx: AnalyzeContext) => {
|
|
||||||
path.traverse(visitor, ctx);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// --- analyze the function props ---
|
|
||||||
const params = fnNode.get('params');
|
|
||||||
const props = params[0];
|
|
||||||
if (props) {
|
|
||||||
if (props.isObjectPattern()) {
|
|
||||||
props.get('properties').forEach(prop => {
|
|
||||||
visitor.Prop?.(prop, context);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Component ${componentNode.name}: The first parameter of the function component must be an object pattern`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- analyze the function body ---
|
|
||||||
const bodyStatements = getFnBody(fnNode).get('body');
|
|
||||||
for (let i = 0; i < bodyStatements.length; i++) {
|
|
||||||
const p = bodyStatements[i];
|
|
||||||
|
|
||||||
const type = p.node.type;
|
|
||||||
|
|
||||||
const visit = visitor[type];
|
|
||||||
if (visit) {
|
|
||||||
// TODO: More type safe way to handle this
|
|
||||||
visit(p as unknown as any, context);
|
|
||||||
} else {
|
|
||||||
context.unhandledNode.push(p.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.isReturnStatement()) {
|
|
||||||
visitor.ReturnStatement?.(p, context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.unhandledNode.length) {
|
|
||||||
addLifecycle(componentNode, WILL_MOUNT, t.blockStatement(context.unhandledNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The process of analyzing the component
|
|
||||||
* 1. identify the component
|
|
||||||
* 2. identify the jsx slice in the component
|
|
||||||
* 2. identify the component's props, including children, alias, and default value
|
|
||||||
* 3. analyze the early return of the component, build into the branch
|
|
||||||
*
|
|
||||||
* @param fnName
|
|
||||||
* @param path
|
|
||||||
* @param options
|
|
||||||
*/
|
|
||||||
export function analyze(
|
|
||||||
fnName: string,
|
|
||||||
path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
|
|
||||||
options: { customAnalyzers?: Analyzer[]; htmlTags: string[] }
|
|
||||||
) {
|
|
||||||
const analyzers = options?.customAnalyzers ? options.customAnalyzers : builtinAnalyzers;
|
|
||||||
|
|
||||||
const root = createComponentNode(fnName, path);
|
|
||||||
analyzeFnComp(path, root, { analyzers, htmlTags: options.htmlTags });
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Bitmap } from './types';
|
|
||||||
import { PropType } from '../constants';
|
|
||||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
|
|
||||||
export function createComponentNode(
|
|
||||||
name: string,
|
|
||||||
fnNode: NodePath<FunctionalExpression>,
|
|
||||||
parent?: ComponentNode
|
|
||||||
): ComponentNode {
|
|
||||||
const comp: ComponentNode = {
|
|
||||||
type: 'comp',
|
|
||||||
level: parent ? parent.level + 1 : 0,
|
|
||||||
name,
|
|
||||||
children: undefined,
|
|
||||||
variables: [],
|
|
||||||
_reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
|
|
||||||
lifecycle: {},
|
|
||||||
parent,
|
|
||||||
fnNode,
|
|
||||||
get ownAvailableVariables() {
|
|
||||||
return [...comp.variables.filter((p): p is ReactiveVariable => p.type === 'reactive')];
|
|
||||||
},
|
|
||||||
get availableVariables() {
|
|
||||||
// Here is critical for the dependency analysis, must put parent's availableVariables first
|
|
||||||
// so the subcomponent can react to the parent's variables change
|
|
||||||
return [...(comp.parent ? comp.parent.availableVariables : []), ...comp.ownAvailableVariables];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return comp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, depBits: number) {
|
|
||||||
// The index of the variable in the availableVariables
|
|
||||||
const idx = comp.availableVariables.length;
|
|
||||||
const bit = 1 << idx;
|
|
||||||
const bitmap = depBits ? depBits | bit : bit;
|
|
||||||
|
|
||||||
comp._reactiveBitMap.set(name, bitmap);
|
|
||||||
comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', depMask: bitmap, level: comp.level });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
|
|
||||||
comp.variables.push({ name, value, type: 'method' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
|
|
||||||
comp.variables.push({ ...subComp, type: 'subComp' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t.BlockStatement) {
|
|
||||||
const compLifecycle = comp.lifecycle;
|
|
||||||
if (!compLifecycle[lifeCycle]) {
|
|
||||||
compLifecycle[lifeCycle] = [];
|
|
||||||
}
|
|
||||||
compLifecycle[lifeCycle]!.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addWatch(
|
|
||||||
comp: ComponentNode,
|
|
||||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
|
|
||||||
depMask: Bitmap
|
|
||||||
) {
|
|
||||||
// if watch not exist, create a new one
|
|
||||||
if (!comp.watch) {
|
|
||||||
comp.watch = [];
|
|
||||||
}
|
|
||||||
comp.watch.push({ callback, depMask });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set<string>) {
|
|
||||||
// TODO: Maybe we should merge
|
|
||||||
comp.usedPropertySet = usedPropertySet;
|
|
||||||
comp.children = view;
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { type NodePath } from '@babel/core';
|
|
||||||
import { AnalyzeContext, Visitor } from './types';
|
|
||||||
import { PropType } from '../constants';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
|
|
||||||
export interface Prop {
|
|
||||||
name: string;
|
|
||||||
type: PropType;
|
|
||||||
alias: string | null;
|
|
||||||
default: t.Expression | null;
|
|
||||||
nestedProps: string[] | null;
|
|
||||||
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze the props deconstructing in the function component
|
|
||||||
* 1. meet identifier, just collect the name
|
|
||||||
* 2. has alias, collect the alias name
|
|
||||||
* 3. has default value, collect the default value
|
|
||||||
* 4. has rest element, collect the rest element
|
|
||||||
* 5. nested destructuring, the e2e goal:
|
|
||||||
* ```js
|
|
||||||
* function(prop1, prop2: [p20, p21]) {}
|
|
||||||
* // transform into
|
|
||||||
* function({ prop1, prop2: [p20, p21] }) {
|
|
||||||
* let p20, p21
|
|
||||||
* watch(() => {
|
|
||||||
* [p20, p21] = prop2
|
|
||||||
* })
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function propsAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
Prop(path: NodePath<t.ObjectProperty | t.RestElement>, ctx) {
|
|
||||||
if (path.isObjectProperty()) {
|
|
||||||
// --- normal property ---
|
|
||||||
const key = path.node.key;
|
|
||||||
const value = path.node.value;
|
|
||||||
if (t.isIdentifier(key) || t.isStringLiteral(key)) {
|
|
||||||
const name = t.isIdentifier(key) ? key.name : key.value;
|
|
||||||
analyzeSingleProp(value, name, path, ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Error(`Unsupported key type in object destructuring: ${key.type}`);
|
|
||||||
} else {
|
|
||||||
// --- rest element ---
|
|
||||||
const arg = path.get('argument');
|
|
||||||
if (!Array.isArray(arg) && arg.isIdentifier()) {
|
|
||||||
addProp(ctx.current, PropType.REST, arg.node.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function analyzeSingleProp(
|
|
||||||
value: t.ObjectProperty['value'],
|
|
||||||
key: string,
|
|
||||||
path: NodePath<t.ObjectProperty>,
|
|
||||||
{ current }: AnalyzeContext
|
|
||||||
) {
|
|
||||||
let defaultVal: t.Expression | null = null;
|
|
||||||
let alias: string | null = null;
|
|
||||||
const nestedProps: string[] | null = [];
|
|
||||||
let nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null;
|
|
||||||
if (t.isIdentifier(value)) {
|
|
||||||
// 1. handle alias without default value
|
|
||||||
// handle alias without default value
|
|
||||||
if (key !== value.name) {
|
|
||||||
alias = value.name;
|
|
||||||
}
|
|
||||||
} else if (t.isAssignmentPattern(value)) {
|
|
||||||
// 2. handle default value case
|
|
||||||
const assignedName = value.left;
|
|
||||||
defaultVal = value.right;
|
|
||||||
if (t.isIdentifier(assignedName)) {
|
|
||||||
if (assignedName.name !== key) {
|
|
||||||
// handle alias in default value case
|
|
||||||
alias = assignedName.name;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Error(`Unsupported assignment type in object destructuring: ${assignedName.type}`);
|
|
||||||
}
|
|
||||||
} else if (t.isObjectPattern(value) || t.isArrayPattern(value)) {
|
|
||||||
// 3. nested destructuring
|
|
||||||
// we should collect the identifier that can be used in the function body as the prop
|
|
||||||
// e.g. function ({prop1, prop2: [p20X, {p211, p212: p212X}]}
|
|
||||||
// we should collect prop1, p20X, p211, p212X
|
|
||||||
path.get('value').traverse({
|
|
||||||
Identifier(path) {
|
|
||||||
// judge if the identifier is a prop
|
|
||||||
// 1. is the key of the object property and doesn't have alias
|
|
||||||
// 2. is the item of the array pattern and doesn't have alias
|
|
||||||
// 3. is alias of the object property
|
|
||||||
const parentPath = path.parentPath;
|
|
||||||
if (parentPath.isObjectProperty() && path.parentKey === 'value') {
|
|
||||||
// collect alias of the object property
|
|
||||||
nestedProps.push(path.node.name);
|
|
||||||
} else if (
|
|
||||||
parentPath.isArrayPattern() ||
|
|
||||||
parentPath.isObjectPattern() ||
|
|
||||||
parentPath.isRestElement() ||
|
|
||||||
(parentPath.isAssignmentPattern() && path.key === 'left')
|
|
||||||
) {
|
|
||||||
// collect the key of the object property or the item of the array pattern
|
|
||||||
nestedProps.push(path.node.name);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
nestedRelationship = value;
|
|
||||||
}
|
|
||||||
addProp(current, PropType.SINGLE, key, defaultVal, alias, nestedProps, nestedRelationship);
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { NodePath } from '@babel/core';
|
|
||||||
import { AnalyzeContext } from '../types';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
import { reactivityFuncNames } from '../../const';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Get all valid dependencies of a babel path
|
|
||||||
* @param propertyKey
|
|
||||||
* @param path
|
|
||||||
* @param ctx
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function getDependenciesFromNode(
|
|
||||||
path: NodePath<t.Expression | t.ClassDeclaration>,
|
|
||||||
{ current }: AnalyzeContext
|
|
||||||
) {
|
|
||||||
// ---- Deps: console.log(count)
|
|
||||||
let depMask = 0;
|
|
||||||
// ---- Assign deps: count = 1 or count++
|
|
||||||
let assignDepMask = 0;
|
|
||||||
const depNodes: Record<string, t.Expression[]> = {};
|
|
||||||
|
|
||||||
const visitor = (innerPath: NodePath<t.Identifier>) => {
|
|
||||||
const propertyKey = innerPath.node.name;
|
|
||||||
const reactiveBitmap = current._reactiveBitMap.get(propertyKey);
|
|
||||||
|
|
||||||
if (reactiveBitmap !== undefined) {
|
|
||||||
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
|
|
||||||
assignDepMask |= reactiveBitmap;
|
|
||||||
} else {
|
|
||||||
depMask |= reactiveBitmap;
|
|
||||||
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
|
|
||||||
depNodes[propertyKey].push(t.cloneNode(innerPath.node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (path.isIdentifier()) {
|
|
||||||
visitor(path);
|
|
||||||
}
|
|
||||||
path.traverse({
|
|
||||||
Identifier: visitor,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Eliminate deps that are assigned in the same method
|
|
||||||
// e.g. { console.log(count); count = 1 }
|
|
||||||
// this will cause infinite loop
|
|
||||||
// so we eliminate "count" from deps
|
|
||||||
if (assignDepMask & depMask) {
|
|
||||||
// TODO: I think we should throw an error here to indicate the user that there is a loop
|
|
||||||
}
|
|
||||||
|
|
||||||
return depMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's the left side of an assignment expression, e.g. count = 1
|
|
||||||
* @param innerPath
|
|
||||||
* @returns assignment expression
|
|
||||||
*/
|
|
||||||
function isAssignmentExpressionLeft(innerPath: NodePath): NodePath | null {
|
|
||||||
let parentPath = innerPath.parentPath;
|
|
||||||
while (parentPath && !parentPath.isStatement()) {
|
|
||||||
if (parentPath.isAssignmentExpression()) {
|
|
||||||
if (parentPath.node.left === innerPath.node) return parentPath;
|
|
||||||
const leftPath = parentPath.get('left') as NodePath;
|
|
||||||
if (innerPath.isDescendant(leftPath)) return parentPath;
|
|
||||||
} else if (parentPath.isUpdateExpression()) {
|
|
||||||
return parentPath;
|
|
||||||
}
|
|
||||||
parentPath = parentPath.parentPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Check if it's a reactivity function, e.g. arr.push
|
|
||||||
* @param innerPath
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function isAssignmentFunction(innerPath: NodePath): boolean {
|
|
||||||
let parentPath = innerPath.parentPath;
|
|
||||||
|
|
||||||
while (parentPath && parentPath.isMemberExpression()) {
|
|
||||||
parentPath = parentPath.parentPath;
|
|
||||||
}
|
|
||||||
if (!parentPath) return false;
|
|
||||||
return (
|
|
||||||
parentPath.isCallExpression() &&
|
|
||||||
parentPath.get('callee').isIdentifier() &&
|
|
||||||
reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type NodePath, types as t } from '@babel/core';
|
|
||||||
import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants';
|
|
||||||
import { ViewParticle } from '@openinula/reactivity-parser';
|
|
||||||
|
|
||||||
export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT;
|
|
||||||
export type Bitmap = number;
|
|
||||||
|
|
||||||
export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression;
|
|
||||||
|
|
||||||
interface BaseVariable<V> {
|
|
||||||
name: string;
|
|
||||||
value: V;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
|
|
||||||
type: 'reactive';
|
|
||||||
level: number;
|
|
||||||
/**
|
|
||||||
* indicate the dependency of the variable | the index of the reactive variable
|
|
||||||
* i.e.
|
|
||||||
* let name = 'John'; // name's bitmap is 0x0001
|
|
||||||
* let age = 18; // age's bitmap is 0x0010
|
|
||||||
* let greeting = `Hello, ${name}`; // greeting's bitmap is 0x0101
|
|
||||||
*/
|
|
||||||
depMask: Bitmap;
|
|
||||||
// need a flag for computed to gen a getter
|
|
||||||
// watch is a static computed
|
|
||||||
isComputed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MethodVariable extends BaseVariable<FunctionalExpression> {
|
|
||||||
type: 'method';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SubCompVariable = ComponentNode<'subComp'>;
|
|
||||||
|
|
||||||
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
|
|
||||||
|
|
||||||
export interface Prop {
|
|
||||||
name: string;
|
|
||||||
type: PropType;
|
|
||||||
alias: string | null;
|
|
||||||
default: t.Expression | null;
|
|
||||||
nestedProps: string[] | null;
|
|
||||||
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComponentNode<Type = 'comp'> {
|
|
||||||
type: Type;
|
|
||||||
name: string;
|
|
||||||
level: number;
|
|
||||||
// The variables defined in the component
|
|
||||||
variables: Variable[];
|
|
||||||
/**
|
|
||||||
* The used properties in the component
|
|
||||||
*/
|
|
||||||
usedPropertySet?: Set<string>;
|
|
||||||
/**
|
|
||||||
* The map to find the reactive bitmap by name
|
|
||||||
*/
|
|
||||||
_reactiveBitMap: Map<string, Bitmap>;
|
|
||||||
/**
|
|
||||||
* The available variables and props owned by the component
|
|
||||||
*/
|
|
||||||
ownAvailableVariables: ReactiveVariable[];
|
|
||||||
/**
|
|
||||||
* The available variables and props for the component and its parent
|
|
||||||
*/
|
|
||||||
availableVariables: ReactiveVariable[];
|
|
||||||
children?: (ComponentNode | ViewParticle)[];
|
|
||||||
parent?: ComponentNode;
|
|
||||||
/**
|
|
||||||
* The function body of the fn component code
|
|
||||||
*/
|
|
||||||
fnNode: NodePath<FunctionalExpression>;
|
|
||||||
lifecycle: Partial<Record<LifeCycle, t.Statement[]>>;
|
|
||||||
/**
|
|
||||||
* The watch fn in the component
|
|
||||||
*/
|
|
||||||
watch?: {
|
|
||||||
depMask: Bitmap;
|
|
||||||
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AnalyzeContext {
|
|
||||||
level: number;
|
|
||||||
current: ComponentNode;
|
|
||||||
analyzers: Analyzer[];
|
|
||||||
htmlTags: string[];
|
|
||||||
traverse: (p: NodePath<t.Statement>, ctx: AnalyzeContext) => void;
|
|
||||||
unhandledNode: t.Statement[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Visitor<S = AnalyzeContext> = {
|
|
||||||
[Type in t.Statement['type']]?: (path: NodePath<Extract<t.Statement, { type: Type }>>, state: S) => void;
|
|
||||||
} & {
|
|
||||||
Prop?: (path: NodePath<t.ObjectProperty | t.RestElement>, state: S) => void;
|
|
||||||
};
|
|
||||||
export type Analyzer = () => Visitor;
|
|
||||||
|
|
||||||
export interface FnComponentDeclaration extends t.FunctionDeclaration {
|
|
||||||
id: t.Identifier;
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NodePath, type types as t } from '@babel/core';
|
|
||||||
import { FnComponentDeclaration } from './types';
|
|
||||||
|
|
||||||
export function isValidPath<T>(path: NodePath<T>): path is NodePath<Exclude<T, undefined | null>> {
|
|
||||||
return !!path.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The component name must be UpperCamelCase
|
|
||||||
export function isValidComponent(node: t.FunctionDeclaration): node is FnComponentDeclaration {
|
|
||||||
// the first letter of the component name must be uppercase
|
|
||||||
return node.id ? isValidComponentName(node.id.name) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidComponentName(name: string) {
|
|
||||||
// the first letter of the component name must be uppercase
|
|
||||||
return /^[A-Z]/.test(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasJSX(path: NodePath<t.Node>) {
|
|
||||||
if (path.isJSXElement()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if there is JSXElement in the children
|
|
||||||
let seen = false;
|
|
||||||
path.traverse({
|
|
||||||
JSXElement() {
|
|
||||||
seen = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return seen;
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
|
|
||||||
*
|
|
||||||
* openInula is licensed under Mulan PSL v2.
|
|
||||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
|
||||||
* You may obtain a copy of Mulan PSL v2 at:
|
|
||||||
*
|
|
||||||
* http://license.coscl.org.cn/MulanPSL2
|
|
||||||
*
|
|
||||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
|
||||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
||||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
||||||
* See the Mulan PSL v2 for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Visitor } from './types';
|
|
||||||
import { addMethod, addProperty, addSubComponent, createComponentNode } from './nodeFactory';
|
|
||||||
import { isValidPath } from './utils';
|
|
||||||
import { type NodePath } from '@babel/core';
|
|
||||||
import { COMPONENT } from '../constants';
|
|
||||||
import { analyzeFnComp } from '.';
|
|
||||||
import { getDependenciesFromNode } from './reactive/getDependencies';
|
|
||||||
import { types as t } from '@openinula/babel-api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* collect all properties and methods from the node
|
|
||||||
* and analyze the dependencies of the properties
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function variablesAnalyze(): Visitor {
|
|
||||||
return {
|
|
||||||
VariableDeclaration(path: NodePath<t.VariableDeclaration>, ctx) {
|
|
||||||
const declarations = path.get('declarations');
|
|
||||||
// iterate the declarations
|
|
||||||
declarations.forEach(declaration => {
|
|
||||||
const id = declaration.get('id');
|
|
||||||
// handle destructuring
|
|
||||||
if (id.isObjectPattern()) {
|
|
||||||
throw new Error('Object destructuring is not supported yet');
|
|
||||||
} else if (id.isArrayPattern()) {
|
|
||||||
// TODO: handle array destructuring
|
|
||||||
throw new Error('Array destructuring is not supported yet');
|
|
||||||
} else if (id.isIdentifier()) {
|
|
||||||
// --- properties: the state / computed / plain properties / methods ---
|
|
||||||
const init = declaration.get('init');
|
|
||||||
let depBits = 0;
|
|
||||||
if (isValidPath(init)) {
|
|
||||||
// handle the method
|
|
||||||
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
|
|
||||||
addMethod(ctx.current, id.node.name, init.node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// handle the subcomponent
|
|
||||||
// Should like Component(() => {})
|
|
||||||
if (
|
|
||||||
init.isCallExpression() &&
|
|
||||||
init.get('callee').isIdentifier() &&
|
|
||||||
(init.get('callee').node as t.Identifier).name === COMPONENT &&
|
|
||||||
(init.get('arguments')[0].isFunctionExpression() || init.get('arguments')[0].isArrowFunctionExpression())
|
|
||||||
) {
|
|
||||||
const fnNode = init.get('arguments')[0] as
|
|
||||||
| NodePath<t.ArrowFunctionExpression>
|
|
||||||
| NodePath<t.FunctionExpression>;
|
|
||||||
const subComponent = createComponentNode(id.node.name, fnNode, ctx.current);
|
|
||||||
|
|
||||||
analyzeFnComp(fnNode, subComponent, ctx);
|
|
||||||
addSubComponent(ctx.current, subComponent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
depBits = getDependenciesFromNode(init, ctx);
|
|
||||||
}
|
|
||||||
addProperty(ctx.current, id.node.name, init.node || null, depBits);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
FunctionDeclaration(path: NodePath<t.FunctionDeclaration>, { current }) {
|
|
||||||
const fnId = path.node.id;
|
|
||||||
if (!fnId) {
|
|
||||||
throw new Error('Function declaration must have an id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const functionExpression = t.functionExpression(
|
|
||||||
path.node.id,
|
|
||||||
path.node.params,
|
|
||||||
path.node.body,
|
|
||||||
path.node.generator,
|
|
||||||
path.node.async
|
|
||||||
);
|
|
||||||
addMethod(current, fnId.name, functionExpression);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue