Match-id-d7656bc63239c6d7836963541689b47670da343c
This commit is contained in:
commit
8449b64222
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||
tabWidth: 2, // tab等2个空格
|
||||
useTabs: false, // 用空格缩进行
|
||||
semi: true, // 行尾使用分号
|
||||
singleQuote: true, // 字符串使用单引号
|
||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||
bracketSpacing: true, // 对象的括号间增加空格
|
||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||
endOfLine: 'lf', // 仅限换行(\n)
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@inula/pinia-adapter",
|
||||
"version": "0.0.1",
|
||||
"description": "pinia adapter",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.23.9",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.23.3",
|
||||
"@cloudsop/horizon": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@vitest/ui": "^0.34.5",
|
||||
"jsdom": "^24.0.0",
|
||||
"vitest": "^0.34.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
|
||||
export * from './pinia';
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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 { createStore } from '@cloudsop/horizon';
|
||||
import { ref } from './ref';
|
||||
|
||||
const storeMap = new Map();
|
||||
|
||||
export function defineStore(id, cbOrDef) {
|
||||
if (!cbOrDef && typeof id === 'object') {
|
||||
cbOrDef = id;
|
||||
id = cbOrDef.id;
|
||||
}
|
||||
|
||||
if (typeof cbOrDef === 'object') {
|
||||
return defineOptionsStore(id, cbOrDef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const data = cbOrDef();
|
||||
|
||||
if (storeMap.has(cbOrDef)) {
|
||||
return storeMap.get(cbOrDef)();
|
||||
}
|
||||
|
||||
const entries = Object.entries(data);
|
||||
|
||||
const state = Object.fromEntries(entries.filter(([key, val]) => val.isRef).map(([key, val]) => [key, val.value]));
|
||||
const computed = Object.fromEntries(
|
||||
entries.filter(([key, val]) => val.isComputed).map(([key, val]) => [key, val.raw])
|
||||
);
|
||||
const actions = Object.fromEntries(entries.filter(([key, val]) => !val.isRef && !val.isComputed));
|
||||
|
||||
const useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
computed,
|
||||
actions: enhanceActions(actions),
|
||||
});
|
||||
|
||||
Object.entries(data)
|
||||
.filter(([key, val]) => val.isRef)
|
||||
.forEach(([key, val]) =>
|
||||
val.watch(newVal => {
|
||||
useStore().$s[key] = newVal;
|
||||
})
|
||||
);
|
||||
|
||||
storeMap.set(cbOrDef, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
function enhanceActions(actions) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(actions).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
function (store, ...args) {
|
||||
return value.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function defineOptionsStore(id, { state, actions, getters }) {
|
||||
if (typeof state === 'function') {
|
||||
state = state();
|
||||
}
|
||||
let useStore = null;
|
||||
|
||||
return () => {
|
||||
if (!useStore) {
|
||||
useStore = createStore({
|
||||
id,
|
||||
state,
|
||||
actions: enhanceActions(actions),
|
||||
computed: getters,
|
||||
});
|
||||
}
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStores(...stores) {
|
||||
const result = {};
|
||||
|
||||
stores.forEach(store => {
|
||||
const expandedStore = store();
|
||||
result[`${expandedStore.id}Store`] = () => expandedStore;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function storeToRefs(store) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(store.$s).map(([key, value]) => {
|
||||
return [key, ref(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function createPinia() {
|
||||
const result = {
|
||||
install: app => {}, // do something?
|
||||
use: plugin => result,
|
||||
state: {}, //
|
||||
};
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = window.horizon.useState(false);
|
||||
|
||||
let state = window.horizon.useRef(initialValue);
|
||||
let listeners = new Set();
|
||||
|
||||
return new Proxy(
|
||||
{ value: initialValue },
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value' || name === 'current') {
|
||||
return state.current;
|
||||
}
|
||||
if (name === Symbol.toPrimitive) {
|
||||
return () => state.current;
|
||||
}
|
||||
if (name === 'isRef') return true;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
if (name === 'raw') return initialValue;
|
||||
},
|
||||
set: (target, name, value) => {
|
||||
if (name === 'value') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
r(!b);
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
return true;
|
||||
} else if (name === 'current') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck For the compiled code.
|
||||
|
||||
import { beforeEach, describe, it, expect } from 'vitest'
|
||||
import {createPinia, defineStore} from '../src/pinia';
|
||||
|
||||
describe('pinia state', () => {
|
||||
beforeEach(() => {
|
||||
});
|
||||
|
||||
const useStore = defineStore('main', {
|
||||
state: () => ({
|
||||
name: 'Eduardo',
|
||||
counter: 0,
|
||||
nested: {n: 0},
|
||||
}),
|
||||
});
|
||||
|
||||
it('can directly access state at the store level', () => {
|
||||
const store = useStore();
|
||||
expect(store.name).toBe('Eduardo');
|
||||
store.name = 'Ed';
|
||||
expect(store.name).toBe('Ed');
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"ts-node": {
|
||||
"esm": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2020 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.
|
||||
*/
|
||||
|
||||
export default {
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
export default function Lazy({ loadingComponent, children }) {
|
||||
const content = window.horizon.useRef(null);
|
||||
const [b, r] = window.horizon.useState(false);
|
||||
const reload = window.horizon.useRef(() => {
|
||||
r(!b);
|
||||
});
|
||||
|
||||
window.horizon.useEffect(() => {
|
||||
reload.current = () => {
|
||||
r(!b);
|
||||
};
|
||||
});
|
||||
|
||||
if (content.current) {
|
||||
return typeof content.current === 'function' ? <component is={content.current} />:content.current
|
||||
} else {
|
||||
setTimeout(()=>{
|
||||
try {
|
||||
const lazy = loadingComponent();
|
||||
lazy.then(function (resolved) {
|
||||
content.current = resolved.default;
|
||||
reload.current();
|
||||
});
|
||||
} catch(err){
|
||||
content.current = loadingComponent;
|
||||
reload.current();
|
||||
}
|
||||
},1)
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { useInjection } from './vue-horizon';
|
||||
let counter = 0;
|
||||
|
||||
export function useLocalStore({ name, data, methods, computed, created, inject }) {
|
||||
const newStore = window.horizon.useRef(true);
|
||||
inject?.forEach(key => {
|
||||
computed[key] = () => {
|
||||
useInjection(key);
|
||||
};
|
||||
});
|
||||
const store = window.horizon.useRef(
|
||||
window.horizon.createStore({
|
||||
id: `${name || 'unknown'}-${Date.now()}-${counter++}`,
|
||||
state: (typeof data === 'function' ? data() : data) || {},
|
||||
actions: {
|
||||
...Object.fromEntries(
|
||||
Object.entries(methods || {}).map(([key, method]) => {
|
||||
return [
|
||||
key,
|
||||
function (state, ...args) {
|
||||
return method.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
),
|
||||
_setValue: function (state, key, value) {
|
||||
state[key.replace(/^this\./, '')] = value;
|
||||
},
|
||||
},
|
||||
computed: computed || {},
|
||||
})
|
||||
);
|
||||
|
||||
if (newStore.current) {
|
||||
requestAnimationFrame(() => {
|
||||
if (created) {
|
||||
created.bind(store.current())();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newStore.current = false;
|
||||
const currentStore = store.current();
|
||||
|
||||
const mix = {
|
||||
...currentStore,
|
||||
...currentStore.$s,
|
||||
...Object.fromEntries(
|
||||
Object.entries(currentStore.$a).map(([key, action]) => {
|
||||
return [
|
||||
key,
|
||||
(...args) => {
|
||||
return action.bind(currentStore)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
Object.entries(currentStore.$c).forEach(([key, computed]) => {
|
||||
const getter = (none, ...args) => {
|
||||
return computed.bind(currentStore)(...args);
|
||||
};
|
||||
|
||||
Object.defineProperty(mix, key, {
|
||||
get: getter,
|
||||
enumerable: true,
|
||||
});
|
||||
});
|
||||
|
||||
return mix;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function forwardRef(render){
|
||||
return window.horizon.forwardRef(render);
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { ref } from './vue-horizon';
|
||||
|
||||
const storeMap = new Map();
|
||||
|
||||
export function defineStore(id, cbOrDef) {
|
||||
if (!cbOrDef && typeof id === 'object') {
|
||||
cbOrDef = id;
|
||||
id = cbOrDef.id;
|
||||
}
|
||||
|
||||
if (typeof cbOrDef === 'object') {
|
||||
return defineOptionsStore(id, cbOrDef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
const data = cbOrDef();
|
||||
|
||||
if (storeMap.has(cbOrDef)) {
|
||||
return storeMap.get(cbOrDef)();
|
||||
}
|
||||
|
||||
const entries = Object.entries(data);
|
||||
|
||||
const state = Object.fromEntries(entries.filter(([key, val]) => val.isRef).map(([key, val]) => [key, val.value]));
|
||||
const computed = Object.fromEntries(
|
||||
entries.filter(([key, val]) => val.isComputed).map(([key, val]) => [key, val.raw])
|
||||
);
|
||||
const actions = Object.fromEntries(entries.filter(([key, val]) => !val.isRef && !val.isComputed));
|
||||
|
||||
const useStore = window.horizon.createStore({
|
||||
id,
|
||||
state,
|
||||
computed,
|
||||
actions: enhanceActions(actions),
|
||||
});
|
||||
|
||||
Object.entries(data)
|
||||
.filter(([key, val]) => val.isRef)
|
||||
.forEach(([key, val]) =>
|
||||
val.watch(newVal => {
|
||||
useStore().$s[key] = newVal;
|
||||
})
|
||||
);
|
||||
|
||||
storeMap.set(cbOrDef, useStore);
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
function enhanceActions(actions) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(actions).map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
function (store, ...args) {
|
||||
return value.bind(this)(...args);
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function defineOptionsStore(id, { state, actions, getters }) {
|
||||
if (typeof state === 'function') {
|
||||
state = state();
|
||||
}
|
||||
let useStore = null;
|
||||
|
||||
return () => {
|
||||
if (!useStore) {
|
||||
useStore = window.horizon.createStore({
|
||||
id,
|
||||
state,
|
||||
actions: enhanceActions(actions),
|
||||
computed: getters,
|
||||
});
|
||||
}
|
||||
|
||||
return useStore();
|
||||
};
|
||||
}
|
||||
|
||||
export function mapStores(...stores) {
|
||||
const result = {};
|
||||
|
||||
stores.forEach(store => {
|
||||
const expandedStore = store();
|
||||
result[`${expandedStore.id}Store`] = () => expandedStore;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function storeToRefs(store) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(store.$s).map(([key, value]) => {
|
||||
return [key, ref(value)];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function createPinia() {
|
||||
const result = {
|
||||
install: app => {}, // do something?
|
||||
use: plugin => result,
|
||||
state: {}, //
|
||||
};
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,688 @@
|
|||
import LazyComponent from './LazyComponent';
|
||||
import { computed } from './vue-horizon';
|
||||
|
||||
const EXACT = 'exact';
|
||||
const DYNAMIC = 'dynamic';
|
||||
|
||||
export let useRouter = () => {
|
||||
throw Error('no router found');
|
||||
};
|
||||
|
||||
export let useRoute = () => {
|
||||
throw Error('no router found');
|
||||
};
|
||||
|
||||
function parseRouteDescriptor(route) {
|
||||
const parts = route.split('/');
|
||||
|
||||
return parts
|
||||
.filter(part => part.length)
|
||||
.map(part => {
|
||||
const result = {};
|
||||
result.raw = part;
|
||||
if (part.startsWith(':')) {
|
||||
const lastChar = part[part.length - 1];
|
||||
result.type = DYNAMIC;
|
||||
if (lastChar === '?') {
|
||||
result.optional = true;
|
||||
part = part.substring(0, part.length - 1);
|
||||
}
|
||||
if (lastChar === '*') {
|
||||
result.optional = true;
|
||||
result.repeat = true;
|
||||
part = part.substring(0, part.length - 1);
|
||||
}
|
||||
if (lastChar === '+') {
|
||||
result.repeat = true;
|
||||
part = part.substring(0, part.length - 1);
|
||||
}
|
||||
|
||||
result.match = part.match(/\((.*)\)$/)?.[1] || '.*';
|
||||
|
||||
result.name = part.match(/^:?(\w*)/)?.[1];
|
||||
} else {
|
||||
result.type = EXACT;
|
||||
if (part[part.length - 1] === '?') {
|
||||
result.optional = true;
|
||||
part = part.substring(0, part.length - 1);
|
||||
}
|
||||
result.match = part;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function parseRoute(route) {
|
||||
const rawRoute = route;
|
||||
const queryRegex = /(?:\?|&)([^=^&]+)(=([^&]+))?/g;
|
||||
const hash = route.match(/#(.+)/)?.[1] || '';
|
||||
route = route.replace('#' + hash, '');
|
||||
|
||||
const queryParams = {};
|
||||
let queryMatch;
|
||||
while ((queryMatch = queryRegex.exec(route))) {
|
||||
value = queryMatch[3];
|
||||
queryParams[queryMatch[1]] = value === undefined ? true : value;
|
||||
}
|
||||
|
||||
const parts = route
|
||||
.replace(/\?.*/g, '')
|
||||
.split('/')
|
||||
.filter(part => part.length);
|
||||
return { queryParams, hash, parts, route: rawRoute, path: route };
|
||||
}
|
||||
|
||||
function matchRoute(route, descriptor) {
|
||||
const parsedRoute = parseRoute(route);
|
||||
let routeIndex = 0;
|
||||
let descriptorIndex = 0;
|
||||
let finished = false;
|
||||
let params = {};
|
||||
while (!finished) {
|
||||
const dPart = descriptor[descriptorIndex];
|
||||
const pPart = parsedRoute.parts[routeIndex];
|
||||
|
||||
if (!dPart) {
|
||||
if (pPart) {
|
||||
return null;
|
||||
}
|
||||
finished = true;
|
||||
continue;
|
||||
}
|
||||
if (!pPart) {
|
||||
if (dPart.optional) {
|
||||
if (dPart.repeat) {
|
||||
params[dPart.name] = [];
|
||||
}
|
||||
descriptorIndex++;
|
||||
continue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (dPart.type === EXACT) {
|
||||
if (dPart.match !== pPart) {
|
||||
if (dPart.optional) {
|
||||
descriptorIndex++;
|
||||
continue;
|
||||
} else return null;
|
||||
}
|
||||
} else {
|
||||
if (dPart.repeat) {
|
||||
const arr = [];
|
||||
if (dPart.match && !pPart.match(new RegExp(dPart.match))) {
|
||||
if (dPart.optional) {
|
||||
descriptorIndex++;
|
||||
params[dPart.name] = [];
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let currentPpart = parsedRoute.parts[routeIndex];
|
||||
const regexp = dPart.match && new RegExp(dPart.match);
|
||||
while (currentPpart) {
|
||||
if (regexp && currentPpart.match(regexp)) {
|
||||
arr.push(currentPpart);
|
||||
routeIndex++;
|
||||
currentPpart = parsedRoute.parts[routeIndex];
|
||||
} else if (regexp) {
|
||||
currentPpart = null;
|
||||
} else {
|
||||
arr.push(currentPpart);
|
||||
routeIndex++;
|
||||
currentPpart = parsedRoute.parts[routeIndex];
|
||||
}
|
||||
}
|
||||
params[dPart.name] = arr.map(item => decodeURIComponent(item));
|
||||
descriptorIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dPart.match && !pPart.match(new RegExp(dPart.match))) {
|
||||
if (dPart.optional) {
|
||||
descriptorIndex++;
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
params[dPart.name] = decodeURIComponent(pPart);
|
||||
}
|
||||
descriptorIndex++;
|
||||
routeIndex++;
|
||||
}
|
||||
if (descriptor[descriptorIndex] && !descriptor[descriptorIndex].optional) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
query: parsedRoute.queryParams,
|
||||
hash: parsedRoute.hash,
|
||||
route: parsedRoute.route,
|
||||
params,
|
||||
};
|
||||
}
|
||||
// {
|
||||
// path: "/user/:id",
|
||||
// children: [
|
||||
// {
|
||||
// path: "/settings",
|
||||
// name: "user-settings",
|
||||
// component: ($route) =>
|
||||
// `Displaying settings for User no.${$route.params.id}`,
|
||||
// },
|
||||
// {
|
||||
// path: "",
|
||||
// name: "user-home",
|
||||
// component: ($route) => `User no.${$route.params.id}`,
|
||||
// },
|
||||
// {
|
||||
// path: "/posts",
|
||||
// name: "user-posts",
|
||||
// component: ($route) =>
|
||||
// `Displaying posts by User no.${$route.params.id}`,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: "",
|
||||
// name: "home",
|
||||
// component: ($route) => "Displaying home",
|
||||
// },
|
||||
// {
|
||||
// path: "/posts",
|
||||
// name: "posts",
|
||||
// component: ($route) => "Displaying posts",
|
||||
// children: [
|
||||
// {
|
||||
// path: "/:postId(\\d+)",
|
||||
// name: "post",
|
||||
// component: ($route) => `Displaing post no.${$route.params.postId}`,
|
||||
// children: [
|
||||
// {
|
||||
// path: "edit",
|
||||
// name: "post-edit",
|
||||
// component: ($route) => `Editing post no.${$route.params.postId}`,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: "new",
|
||||
// name: "post-add",
|
||||
// component: () => "Adding new post",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// path: "/:dirpath(.*)*",
|
||||
// component: ($route) =>
|
||||
// `error resolving path at /${$route.params.dirpath.join("/")}`,
|
||||
// },
|
||||
// ];
|
||||
|
||||
export function createMemoryHistory() {
|
||||
let history = ['/'];
|
||||
let pointer = 0;
|
||||
|
||||
function push(path, flags = {}) {
|
||||
const { replace } = flags;
|
||||
if (replace && history.length) {
|
||||
history[pointer] = path;
|
||||
history.length = pointer + 1;
|
||||
} else if (typeof path === 'string') {
|
||||
history.length = pointer + 1;
|
||||
history[pointer + 1] = path;
|
||||
pointer++;
|
||||
history.length = pointer + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function replace(path, flags = {}) {
|
||||
flags.replace = true;
|
||||
push(path, flags);
|
||||
}
|
||||
|
||||
function go(steps) {
|
||||
if (steps > 0) {
|
||||
let i = 0;
|
||||
while (i < steps) {
|
||||
if (pointer + 1 < history.length) {
|
||||
pointer++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else if (steps < 0) {
|
||||
let i = 0;
|
||||
while (i > steps) {
|
||||
if (pointer > 0) {
|
||||
pointer--;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get() {
|
||||
return history[pointer];
|
||||
}
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
get,
|
||||
addListener: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
export function createWebHashHistory() {
|
||||
const listeners = [];
|
||||
window.addEventListener('popstate', event => {
|
||||
listeners.forEach(listener => listener(window.location.hash.substring(1)));
|
||||
});
|
||||
|
||||
function push(path, flags = {}) {
|
||||
if (flags.replace) {
|
||||
window.history.replaceState(null, null, `#${path}`);
|
||||
window.location.hash = path;
|
||||
} else {
|
||||
window.history.pushState(null, null, `#${path}`);
|
||||
window.location.hash = path;
|
||||
}
|
||||
}
|
||||
|
||||
function replace(path, flags = {}) {
|
||||
window.history.replaceState(null, null, `#${path}`);
|
||||
window.location.hash = path;
|
||||
}
|
||||
|
||||
function go(steps) {
|
||||
window.history.go(steps);
|
||||
}
|
||||
|
||||
function get() {
|
||||
return window.location.hash.substring(1);
|
||||
}
|
||||
|
||||
function addListener(listener) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
get,
|
||||
addListener,
|
||||
};
|
||||
}
|
||||
|
||||
export function createWebHistory() {
|
||||
const host = `${window.location.protocol}//${window.location.host}`;
|
||||
const listeners = [];
|
||||
window.addEventListener('popstate', event => {
|
||||
event.preventDefault();
|
||||
listeners.forEach(listener => listener(window.location.pathname));
|
||||
});
|
||||
|
||||
function push(path, flags = {}) {
|
||||
if (flags.replace) {
|
||||
window.history.replaceState(null, null, path);
|
||||
} else {
|
||||
window.history.pushState(null, null, path);
|
||||
}
|
||||
}
|
||||
|
||||
function replace(path, flags = {}) {
|
||||
window.history.replaceState(null, null, path);
|
||||
}
|
||||
|
||||
function go(steps) {
|
||||
window.history.go(steps);
|
||||
}
|
||||
|
||||
function get() {
|
||||
return window.location.href.replace(host, '');
|
||||
}
|
||||
|
||||
function addListener(listener) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
return {
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
get,
|
||||
addListener,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRouter({ routes, history }) {
|
||||
let counter = 0;
|
||||
const directRoutes = {};
|
||||
let DepthContext;
|
||||
|
||||
function Redirect({ fallback, from, to, ...props }) {
|
||||
if (match(history.get()) === match(from) && history.get() !== to) {
|
||||
setTimeout(() => {
|
||||
// rendered.current = false;
|
||||
history.replace(to);
|
||||
triggerListeners();
|
||||
}, 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
const child = <LazyComponent loadingComponent={fallback}>Loading...</LazyComponent>;
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
function processRoutes(routes, base = '/') {
|
||||
routes.forEach(route => {
|
||||
const absolutePath = base + '/' + route.path;
|
||||
const name = route.name || `_unnamedRoute${counter++}`;
|
||||
if (route.component) {
|
||||
directRoutes[name] = {
|
||||
...route,
|
||||
component: Array.isArray(route.component)
|
||||
? route.component
|
||||
: [
|
||||
function LazyWrapper() {
|
||||
return <LazyComponent loadingComponent={route.component}>Loading...</LazyComponent>;
|
||||
},
|
||||
],
|
||||
path: absolutePath.replaceAll(/\/+/g, '/'),
|
||||
descriptor: parseRouteDescriptor(absolutePath),
|
||||
meta: route.meta,
|
||||
};
|
||||
}
|
||||
if (route.children) {
|
||||
processChildRoutes(route.children, [directRoutes[name]], base);
|
||||
}
|
||||
if (route.redirect) {
|
||||
directRoutes[name] = {
|
||||
...route,
|
||||
component: [
|
||||
function RedirectWrapper() {
|
||||
return <Redirect from={route.path} to={route.redirect} fallback={route.component} />;
|
||||
},
|
||||
],
|
||||
path: absolutePath.replaceAll(/\/+/g, '/'),
|
||||
descriptor: parseRouteDescriptor(absolutePath),
|
||||
meta: route.meta,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processChildRoutes(routes, parents, base) {
|
||||
processRoutes(
|
||||
routes.map(route => {
|
||||
const processed = { ...route };
|
||||
const lastParent = parents[parents.length - 1];
|
||||
if (!route.path.startsWith(lastParent.path)) {
|
||||
processed.path = /*lastParent +*/ route.path;
|
||||
}
|
||||
if (route.redirect) {
|
||||
processed.component = (lastParent.component || []).concat([
|
||||
function RedirectWrapperChild() {
|
||||
return <Redirect from={route.path} to={route.redirect} fallback={route.component} />;
|
||||
},
|
||||
]);
|
||||
} else if (route.component) {
|
||||
processed.component = (lastParent.component || []).concat([
|
||||
function LazyWrapperChild() {
|
||||
return <LazyComponent loadingComponent={route.component}>Loading...</LazyComponent>;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return processed;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
processRoutes(routes);
|
||||
|
||||
function match(path = '') {
|
||||
let matched;
|
||||
Object.entries(directRoutes)
|
||||
.filter(([name, route]) => {
|
||||
const match = matchRoute(path, route.descriptor);
|
||||
if (match) {
|
||||
matched = name;
|
||||
}
|
||||
return match;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b[1].descriptor.length - a[1].descriptor.length;
|
||||
})[0];
|
||||
return matched;
|
||||
}
|
||||
|
||||
function resolve({ path, query, params, hash, name }) {
|
||||
let descriptor;
|
||||
if (path) {
|
||||
descriptor = parseRouteDescriptor(path);
|
||||
} else if (name) {
|
||||
descriptor = directRoutes[name].descriptor;
|
||||
}
|
||||
let result = '';
|
||||
descriptor.forEach(part => {
|
||||
if (part.type === EXACT) {
|
||||
result += '/' + part.match;
|
||||
} else if (part.type === DYNAMIC) {
|
||||
result += '/' + params[part.name];
|
||||
}
|
||||
});
|
||||
if (query) {
|
||||
result +=
|
||||
'?' +
|
||||
Object.entries(query)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&');
|
||||
}
|
||||
if (hash) {
|
||||
result += `#${hash}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function push(path, flags) {
|
||||
if (typeof path === 'string') {
|
||||
history.push(path, flags);
|
||||
} else {
|
||||
history.push(resolve(path), flags);
|
||||
}
|
||||
triggerListeners();
|
||||
}
|
||||
|
||||
function replace(path, flags) {
|
||||
history.push(path, flags);
|
||||
triggerListeners();
|
||||
}
|
||||
|
||||
function go(steps) {
|
||||
history.go(steps);
|
||||
triggerListeners();
|
||||
}
|
||||
|
||||
function get() {
|
||||
return {
|
||||
view: match(history.get()),
|
||||
route: parseRoute(history.get()),
|
||||
descriptor: directRoutes[match(history.get())].descriptor,
|
||||
match: matchRoute(history.get(), directRoutes[match(history.get())].descriptor),
|
||||
component: [directRoutes[match(history.get())].component],
|
||||
};
|
||||
}
|
||||
|
||||
let listenerId = 0;
|
||||
const listeners = new Map();
|
||||
|
||||
const hookListeners = new Set();
|
||||
|
||||
function addHookListener(guard) {
|
||||
hookListeners.add(guard);
|
||||
return () => {
|
||||
hookListeners.delete(guard);
|
||||
};
|
||||
}
|
||||
|
||||
const afterEachListeners = new Set();
|
||||
|
||||
const beforeEachListeners = new Set();
|
||||
|
||||
const beforeResolveListeners = new Set();
|
||||
|
||||
const errorListeners = new Set();
|
||||
|
||||
function triggerListeners() {
|
||||
listeners.forEach(listener => listener());
|
||||
|
||||
Array.from(hookListeners.values()).forEach(listener => listener());
|
||||
}
|
||||
|
||||
history.addListener(triggerListeners);
|
||||
|
||||
useRoute = () => {
|
||||
const current = get();
|
||||
const routeName = match(history.get());
|
||||
|
||||
const [b, r] = window.horizon.useState(false);
|
||||
window.horizon.useEffect(() => {
|
||||
return addHookListener(() => {
|
||||
r(!b);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
fullPath: window.location.href,
|
||||
hash: window.location.hash,
|
||||
matched: current,
|
||||
name: routeName,
|
||||
params: current.match.params,
|
||||
path: window.location.pathname,
|
||||
query: current.match.query,
|
||||
};
|
||||
};
|
||||
|
||||
useRouter = () => ({
|
||||
get currentRoute() {
|
||||
return computed(() => {
|
||||
return get().route;
|
||||
});
|
||||
},
|
||||
listening: true, // TODO: implement
|
||||
get options() {
|
||||
throw Error('not implemented'); // TODO: implement
|
||||
},
|
||||
addRoute(parentName, route) {
|
||||
throw Error('not implemented'); // TODO: implement
|
||||
},
|
||||
afterEach(guard) {
|
||||
afterEachListeners.add(guard);
|
||||
return () => {
|
||||
afterEachListeners.delete(guard);
|
||||
};
|
||||
},
|
||||
back() {
|
||||
go(-1);
|
||||
},
|
||||
beforeEach(guard) {
|
||||
beforeEachListeners.add(guard);
|
||||
return () => {
|
||||
beforeEachListeners.delete(guard);
|
||||
};
|
||||
},
|
||||
beforeResolve(guard) {
|
||||
beforeResolveListeners.add(guard);
|
||||
return () => {
|
||||
beforeResolveListeners.delete(guard);
|
||||
};
|
||||
},
|
||||
forward() {
|
||||
go(1);
|
||||
},
|
||||
getRoutes() {
|
||||
return directRoutes;
|
||||
},
|
||||
go,
|
||||
hasRoute(name) {
|
||||
return !!routes[name];
|
||||
},
|
||||
isReady() {
|
||||
return true; // NOTE: without server side rendering router is always ready
|
||||
},
|
||||
onError(guard) {
|
||||
errorListeners.add(guard);
|
||||
return () => {
|
||||
errorListeners.delete(guard);
|
||||
};
|
||||
},
|
||||
push,
|
||||
removeRoute(name) {
|
||||
delete directRoutes[name];
|
||||
},
|
||||
replace(to) {
|
||||
replace(to);
|
||||
},
|
||||
resolve,
|
||||
match,
|
||||
});
|
||||
|
||||
function RouterLink({ replace, to, children }) {
|
||||
return (
|
||||
<a
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
push(to, { replace });
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function RouterView(props) {
|
||||
console.log('router view', { props });
|
||||
if (!DepthContext) {
|
||||
DepthContext = window.horizon.createContext(0);
|
||||
}
|
||||
|
||||
let depth = window.horizon.useContext(DepthContext);
|
||||
const [boo, re] = window.horizon.useState(true);
|
||||
window.horizon.useEffect(() => {
|
||||
const id = listenerId++;
|
||||
listeners.set(id, () => {
|
||||
re(!boo);
|
||||
});
|
||||
|
||||
return () => {
|
||||
listeners.delete(id);
|
||||
};
|
||||
});
|
||||
|
||||
const result = directRoutes[match(history.get())];
|
||||
|
||||
const View = () => {
|
||||
console.log('View', { result });
|
||||
return window.horizon.createElement(
|
||||
result.component[depth],
|
||||
result.props ? { ...result.route } : { $route: result.route }
|
||||
);
|
||||
};
|
||||
|
||||
const finalView = (
|
||||
<DepthContext.Provider value={depth + 1}>
|
||||
<View />
|
||||
</DepthContext.Provider>
|
||||
);
|
||||
|
||||
return finalView;
|
||||
}
|
||||
|
||||
return { RouterLink, RouterView };
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
export function nextTick(callback) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
export function onMounted(callback) {
|
||||
window.horizon.useEffect(() => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
export function onBeforeUnmount(callback) {
|
||||
window.horizon.useEffect(() => {
|
||||
return () => {
|
||||
callback();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function onUnmounted(callback) {
|
||||
window.horizon.useEffect(() => {
|
||||
return () => {
|
||||
callback();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function toRef(val) {
|
||||
if (typeof val === 'function') {
|
||||
return computed(val);
|
||||
}
|
||||
|
||||
if (val.toRef) {
|
||||
return val;
|
||||
}
|
||||
|
||||
return ref(val);
|
||||
}
|
||||
|
||||
export function unref(val) {
|
||||
if (val.isRef) {
|
||||
return val.value;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
export function toRefs(obj) {
|
||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, toRef(value)]));
|
||||
}
|
||||
|
||||
export function ref(initialValue) {
|
||||
let [b, r] = window.horizon.useState(false);
|
||||
|
||||
let state = window.horizon.useRef(initialValue);
|
||||
let listeners = new Set();
|
||||
|
||||
return new Proxy(
|
||||
{ value: initialValue },
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value' || name === 'current') {
|
||||
return state.current;
|
||||
}
|
||||
if (name === Symbol.toPrimitive) {
|
||||
return () => state.current;
|
||||
}
|
||||
if (name === 'isRef') return true;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
if (name === 'raw') return initialValue;
|
||||
},
|
||||
set: (target, name, value) => {
|
||||
if (name === 'value') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
r(!b);
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
return true;
|
||||
} else if (name === 'current') {
|
||||
if (state.current === value) return true;
|
||||
state.current = value;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function reactive(initialValue) {
|
||||
let [b, r] = window.horizon.useState(false);
|
||||
const data = window.horizon.useRef(initialValue);
|
||||
const listeners = new Set();
|
||||
|
||||
return new Proxy(initialValue, {
|
||||
set: (target, name, value) => {
|
||||
r(!b);
|
||||
Array.from(listeners).forEach(listener => listener(value, data.current[name]));
|
||||
data.current[name] = value;
|
||||
return true;
|
||||
},
|
||||
get: (target, name) => {
|
||||
if (name === 'isRef') return true;
|
||||
if (name === 'watch')
|
||||
return listener => {
|
||||
listeners.add(listener);
|
||||
};
|
||||
return data.current[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function computed(foo) {
|
||||
if (typeof foo === 'object') return writtableComputed(foo);
|
||||
let listeners = new Set();
|
||||
let previousValue = foo();
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value') {
|
||||
const newValue = foo();
|
||||
if (JSON.stringify(newValue) !== JSON.stringify(previousValue)) {
|
||||
previousValue = newValue;
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(newValue);
|
||||
});
|
||||
}
|
||||
return previousValue;
|
||||
}
|
||||
if (name === 'isComputed') return true;
|
||||
if (name === 'raw') return () => foo;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function writtableComputed(methods) {
|
||||
let listeners = new Set();
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (target, name) => {
|
||||
if (name === 'value') return methods.get();
|
||||
if (name === 'isComputed') return true;
|
||||
if (name === 'raw') return () => foo;
|
||||
if (name === 'watch')
|
||||
return cb => {
|
||||
listeners.add(cb);
|
||||
};
|
||||
},
|
||||
set: (target, name, value) => {
|
||||
methods.set(value);
|
||||
Array.from(listeners.values()).forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const globals = new Map();
|
||||
|
||||
export function provide(key, value) {
|
||||
globals.set(key, value);
|
||||
}
|
||||
|
||||
export function inject(key) {
|
||||
return globals.get(key);
|
||||
}
|
||||
|
||||
export const useInjection = inject;
|
||||
|
||||
export function Teleport({ to, children }) {
|
||||
const container = window.horizon.useState(() => {
|
||||
document.createElement('div');
|
||||
});
|
||||
|
||||
window.horizon.useEffect(() => {
|
||||
to.appendChild(container);
|
||||
|
||||
return () => {
|
||||
to.removeChild(container);
|
||||
};
|
||||
}, [to, container]);
|
||||
|
||||
return window.horizon.createPortal(children, container);
|
||||
}
|
||||
|
||||
export function defineProps(props) {
|
||||
console.log('define props', props);
|
||||
}
|
||||
|
||||
export function getCurrentInstance() {
|
||||
return window.horizon.getProcessingVNode();
|
||||
}
|
||||
|
||||
export function toRaw(maybeRef) {
|
||||
if (maybeRef.isRef || maybeRef.isComputed) return maybeRef.raw;
|
||||
else return maybeRef;
|
||||
}
|
||||
|
||||
export function watch(data, cb, options) {
|
||||
if (data.isRef || data.isComputed) {
|
||||
data.watch(cb);
|
||||
} else if (typeof data === 'function') {
|
||||
watch(computed(data), cb, options);
|
||||
} else {
|
||||
throw Error('Watch method not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export function useWindowSize(options = {}) {
|
||||
const {
|
||||
initialWidth = Number.POSITIVE_INFINITY,
|
||||
initialHeight = Number.POSITIVE_INFINITY,
|
||||
listenOrientation = true,
|
||||
includeScrollbar = true,
|
||||
} = options;
|
||||
|
||||
const width = ref(initialWidth);
|
||||
const height = ref(initialHeight);
|
||||
|
||||
function update() {
|
||||
if (window) {
|
||||
if (includeScrollbar) {
|
||||
width.value = window.innerWidth;
|
||||
height.value = window.innerHeight;
|
||||
} else {
|
||||
width.value = window.document.documentElement.clientWidth;
|
||||
height.value = window.document.documentElement.clientHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
window.horizon.useEffect(() => {
|
||||
window.addEventListener('resize', update);
|
||||
if (listenOrientation) window.addEventListener('orientationchange', update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
if (listenOrientation) window.removeEventListener('orientationchange', update);
|
||||
};
|
||||
});
|
||||
|
||||
return { width: width.value, height: height.value };
|
||||
}
|
||||
|
||||
export function defineComponent(data) {
|
||||
console.log('define component', data);
|
||||
return {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function defineEmits(props, emitKeys) {
|
||||
const listeners = {};
|
||||
|
||||
const eventprops = Object.entries(props)
|
||||
.filter(([propname, listener]) => propname.startsWith('on'))
|
||||
.map(([propname, listener]) => [propname.substring(2).toLowerCase(), listener]);
|
||||
|
||||
emitKeys.forEach(eventName => {
|
||||
const key = eventName.toLowerCase();
|
||||
listeners[key] = eventprops.find(evt => evt[0] === key)?.[1] || (() => {});
|
||||
});
|
||||
|
||||
return (event, ...data) => {
|
||||
if (listeners[event.toLowerCase()]) {
|
||||
listeners[event.toLowerCase()](...data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function defineExpose(props, expose) {
|
||||
props.expose = expose;
|
||||
}
|
||||
|
||||
export function createVNode(component, props) {
|
||||
return window.horizon.createElement(component, props, props.children);
|
||||
}
|
||||
|
||||
export function render(vnode, target) {
|
||||
window.horizon.render(vnode, target);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@opentiny/react-common",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@opentiny/vue-renderless": "workspace:~",
|
||||
"@opentiny/vue-theme": "workspace:~",
|
||||
"classnames": "^2.3.2",
|
||||
"react": "18.2.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
"@vue/runtime-core": "^3.3.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
interface CssClassObject {
|
||||
[k: string]: any
|
||||
}
|
||||
type CssClassArray = Array<string | CssClassObject>
|
||||
export type CssClass = string | CssClassObject | CssClassArray
|
||||
|
||||
/**
|
||||
* 简单合并 tailwind 类对象为字符串值
|
||||
*
|
||||
* @param cssClassObject tailwind 类对象
|
||||
* @returns string
|
||||
*/
|
||||
const stringifyCssClassObject = (cssClassObject: CssClassObject): string => {
|
||||
const allCssClass: Array<string> = []
|
||||
|
||||
Object.keys(cssClassObject).forEach((cssClass) => cssClassObject[cssClass] && allCssClass.push(cssClass))
|
||||
|
||||
return allCssClass.join('\u{20}')
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单合并 tailwind 类数组为字符串值
|
||||
*
|
||||
* @param cssClassArray tailwind 类数组
|
||||
* @returns string
|
||||
*/
|
||||
const stringifyCssClassArray = (cssClassArray: CssClassArray): string => {
|
||||
const allCssClass: Array<string> = []
|
||||
|
||||
cssClassArray.forEach((cssClass) => {
|
||||
if (typeof cssClass === 'string') {
|
||||
allCssClass.push(cssClass)
|
||||
} else if (typeof cssClass === 'object') {
|
||||
allCssClass.push(stringifyCssClassObject(cssClass))
|
||||
}
|
||||
})
|
||||
|
||||
return allCssClass.join('\u{20}')
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单合并 tailwind 类对象为字符串值,去重处理留给 tailwind-merge 处理
|
||||
*
|
||||
* @param {*} cssClasses tailwind 类集合
|
||||
* @returns string
|
||||
*/
|
||||
export const stringifyCssClass = (cssClasses: Array<CssClass>): string => {
|
||||
if (!cssClasses || (Array.isArray(cssClasses) && !cssClasses.length)) return ''
|
||||
|
||||
const allCssClass: Array<string> = []
|
||||
|
||||
cssClasses.forEach((cssClass) => {
|
||||
if (cssClass) {
|
||||
if (typeof cssClass === 'string') {
|
||||
allCssClass.push(cssClass)
|
||||
} else if (Array.isArray(cssClass)) {
|
||||
allCssClass.push(stringifyCssClassArray(cssClass))
|
||||
} else if (typeof cssClass === 'object') {
|
||||
allCssClass.push(stringifyCssClassObject(cssClass))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return allCssClass.join('\u{20}')
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { eventBus } from './utils'
|
||||
|
||||
const $busMap = new Map()
|
||||
|
||||
export const emit =
|
||||
(props) =>
|
||||
(evName, ...args) => {
|
||||
const reactEvName = 'on' + evName.substr(0, 1).toUpperCase() + evName.substr(1)
|
||||
|
||||
if (props[reactEvName] && typeof props[reactEvName] === 'function') {
|
||||
props[reactEvName](...args)
|
||||
} else {
|
||||
const $bus = $busMap.get(props)
|
||||
if ($bus) {
|
||||
$bus.emit(evName, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
export const on = (props) => (evName, callback) => {
|
||||
if ($busMap.get(props)) {
|
||||
const $bus = $busMap.get(props)
|
||||
$bus.on(evName, callback)
|
||||
} else {
|
||||
const $bus = eventBus()
|
||||
$bus.on(evName, callback)
|
||||
$busMap.set(props, $bus)
|
||||
}
|
||||
}
|
||||
export const off = (props) => (evName, callback) => {
|
||||
const $bus = $busMap.get(props)
|
||||
if (!$bus) return
|
||||
$bus.off(evName, callback)
|
||||
}
|
||||
export const once = (props) => (evName, callback) => {
|
||||
let $bus = null
|
||||
const onceCallback = (...args) => {
|
||||
callback(...args)
|
||||
$bus && $bus.off(evName, onceCallback)
|
||||
}
|
||||
|
||||
if ($busMap.get(props)) {
|
||||
$bus = $busMap.get(props)
|
||||
$bus.on(evName, onceCallback)
|
||||
} else {
|
||||
$bus = eventBus()
|
||||
$bus.on(evName, onceCallback)
|
||||
$busMap.set(props, $bus)
|
||||
}
|
||||
}
|
||||
export const emitEvent = (vm) => {
|
||||
const broadcast = (vm, componentName, eventName, ...args) => {
|
||||
const children = vm.$children
|
||||
|
||||
Array.isArray(children) &&
|
||||
children.forEach((child) => {
|
||||
const name = child.$options && child.$options.componentName
|
||||
const component = child
|
||||
|
||||
if (name === componentName) {
|
||||
component.emit(eventName, ...args)
|
||||
// todo: 调研 component.$emitter
|
||||
// component.$emitter && component.$emitter.emit(eventName, params)
|
||||
} else {
|
||||
broadcast(child, componentName, eventName, ...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
dispatch(componentName, eventName, ...args) {
|
||||
let parent = vm.$parent
|
||||
if (parent.type === null) return
|
||||
let name = parent.$options && parent.$options.componentName
|
||||
while (parent && parent.type && (!name || name !== componentName)) {
|
||||
parent = parent.$parent
|
||||
|
||||
if (parent) name = parent.$options && parent.$options.componentName
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
parent.emit(eventName, ...args)
|
||||
// fix: VUE3下事件参数为数组,VUE2下事件参数不是数组,这里修改为和VUE2兼容
|
||||
// parent.$emitter && parent.$emitter.emit(...[eventName].concat(params))
|
||||
}
|
||||
},
|
||||
broadcast(componentName, eventName, ...args) {
|
||||
broadcast(vm, componentName, eventName, ...args)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import { useRef, useEffect, useState } from 'react'
|
||||
import { compWhiteList } from './virtual-comp'
|
||||
|
||||
export function getFiberByDom(dom) {
|
||||
const key = Object.keys(dom).find((key) => {
|
||||
return (
|
||||
key.startsWith('__reactFiber$') || // react 17+
|
||||
key.startsWith('__reactInternalInstance$')
|
||||
) // react <17
|
||||
})
|
||||
|
||||
return dom[key]
|
||||
}
|
||||
|
||||
function defaultBreaker({ type }) {
|
||||
if (type && typeof type !== 'string') {
|
||||
return !compWhiteList.includes(type.name)
|
||||
}
|
||||
}
|
||||
|
||||
export function traverseFiber(fiber, handler, breaker = defaultBreaker) {
|
||||
if (!fiber) return
|
||||
typeof handler === 'function' && handler(fiber)
|
||||
Array.isArray(handler) &&
|
||||
handler.forEach((task) => {
|
||||
typeof task === 'function' && task(fiber)
|
||||
})
|
||||
traverseFiber(fiber.sibling, handler, breaker)
|
||||
breaker(fiber) || traverseFiber(fiber.child, handler, breaker)
|
||||
}
|
||||
|
||||
const parentMap = new WeakMap()
|
||||
export function getParentFiber(fiber, isFirst = true, child = fiber) {
|
||||
if (!fiber || !fiber.return) return null
|
||||
if (parentMap.has(child)) return parentMap.get(child)
|
||||
if (fiber.type && typeof fiber.type !== 'string' && !isFirst) {
|
||||
parentMap.set(child, fiber)
|
||||
return fiber
|
||||
}
|
||||
return getParentFiber(fiber.return, false, fiber)
|
||||
}
|
||||
|
||||
export function creatFiberCombine(fiber) {
|
||||
if (!fiber) return
|
||||
const refs = {}
|
||||
const children = []
|
||||
|
||||
traverseFiber(fiber.child, [
|
||||
(fiber) => {
|
||||
if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v_ref')) {
|
||||
refs[fiber.stateNode.getAttribute('v_ref')] = fiber.stateNode
|
||||
} else if (fiber.memoizedProps.v_ref) {
|
||||
refs[fiber.memoizedProps.v_ref] = fiber
|
||||
}
|
||||
},
|
||||
(fiber) => {
|
||||
if (fiber.type && typeof fiber.type !== 'string') {
|
||||
children.push(fiber)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
return {
|
||||
fiber,
|
||||
refs,
|
||||
children
|
||||
}
|
||||
}
|
||||
|
||||
export function useFiber() {
|
||||
const ref = useRef()
|
||||
const [parent, setParent] = useState()
|
||||
const [current, setCurrent] = useState()
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const current_fiber = getFiberByDom(ref.current)
|
||||
setParent(getParentFiber(current_fiber.return))
|
||||
setCurrent(current_fiber.return)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
ref,
|
||||
parent: creatFiberCombine(parent),
|
||||
current: creatFiberCombine(current)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { useState, useRef } from 'react'
|
||||
|
||||
export function useExcuteOnce(cb, ...args) {
|
||||
const isExcuted = useRef(false)
|
||||
const result = useRef()
|
||||
if (!isExcuted.current) {
|
||||
isExcuted.current = true
|
||||
result.current = cb(...args)
|
||||
}
|
||||
return result.current
|
||||
}
|
||||
|
||||
export function useReload() {
|
||||
const [_, reload] = useState(0)
|
||||
return () => reload((pre) => pre + 1)
|
||||
}
|
||||
|
||||
export function useOnceResult(func, ...args) {
|
||||
const result = useRef()
|
||||
if (!result.current) {
|
||||
result.current = func(...args)
|
||||
}
|
||||
return result.current
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import { Svg } from './svg-render'
|
||||
import { generateVueHooks, useVueLifeHooks } from './vue-hooks.js'
|
||||
import { emitEvent } from './event.js'
|
||||
import { If, Component, Slot, For, Transition } from './virtual-comp'
|
||||
import { filterAttrs, vc, getElementCssClass, eventBus } from './utils.js'
|
||||
import { useFiber } from './fiber.js'
|
||||
import { useVm } from './vm.js'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { stringifyCssClass } from './csscls.js'
|
||||
import { useExcuteOnce, useReload, useOnceResult } from './hooks.js'
|
||||
|
||||
// 导入 vue 响应式系统
|
||||
import { effectScope, nextTick, reactive } from '@vue/runtime-core'
|
||||
import { useCreateVueInstance } from './vue-instance'
|
||||
|
||||
import '@opentiny/vue-theme/base/index.less'
|
||||
|
||||
// emitEvent, dispath, broadcast
|
||||
export const $prefix = 'Tiny'
|
||||
|
||||
export const $props = {
|
||||
'tiny_mode': String,
|
||||
'tiny_mode_root': Boolean,
|
||||
'tiny_template': [Function, Object],
|
||||
'tiny_renderless': Function,
|
||||
'tiny_theme': String,
|
||||
'tiny_chart_theme': Object
|
||||
}
|
||||
|
||||
export const mergeClass = (...cssClasses) => twMerge(stringifyCssClass(cssClasses))
|
||||
|
||||
const setup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants, vm, parent, $bus }) => {
|
||||
const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless
|
||||
const { dispatch, broadcast } = emitEvent(vm)
|
||||
|
||||
const utils = {
|
||||
vm,
|
||||
parent,
|
||||
emit: vm.$emit,
|
||||
constants,
|
||||
nextTick,
|
||||
dispatch,
|
||||
broadcast,
|
||||
t() {},
|
||||
mergeClass,
|
||||
mode: props.tiny_mode
|
||||
}
|
||||
|
||||
const sdk = render(
|
||||
props,
|
||||
{
|
||||
...generateVueHooks({
|
||||
$bus
|
||||
})
|
||||
},
|
||||
utils,
|
||||
extendOptions
|
||||
)
|
||||
|
||||
const attrs = {
|
||||
a: filterAttrs,
|
||||
m: mergeClass,
|
||||
vm: utils.vm,
|
||||
gcls: (key) => getElementCssClass(classes, key)
|
||||
}
|
||||
|
||||
if (Array.isArray(api)) {
|
||||
api.forEach((name) => {
|
||||
const value = sdk[name]
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
attrs[name] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
export const useSetup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants }) => {
|
||||
const $bus = useOnceResult(() => eventBus())
|
||||
|
||||
// 刷新逻辑
|
||||
const reload = useReload()
|
||||
useExcuteOnce(() => {
|
||||
// 1. 响应式触发 $bus 的事件
|
||||
// 2. 事件响应触发组件更新
|
||||
$bus.on('event:reload', reload)
|
||||
})
|
||||
|
||||
// 收集副作用,组件卸载自动清除副作用
|
||||
const scope = useOnceResult(() => effectScope())
|
||||
useExcuteOnce(() => {
|
||||
$bus.on('hook:onBeforeUnmount', () => scope.stop())
|
||||
})
|
||||
|
||||
// 创建响应式 props,每次刷新更新响应式 props
|
||||
const reactiveProps = useOnceResult(() => reactive(props))
|
||||
Object.assign(reactiveProps, props)
|
||||
|
||||
const { ref, vm } = useCreateVueInstance({
|
||||
$bus,
|
||||
props
|
||||
})
|
||||
|
||||
// 执行一次 renderless
|
||||
// renderless 作为 setup 的结果,最后要将结果挂在 vm 上
|
||||
let setupResult = useExcuteOnce(() => {
|
||||
let result
|
||||
// 在 effectScope 里运行 renderless
|
||||
scope.run(() => {
|
||||
result = setup({
|
||||
props: reactiveProps,
|
||||
renderless,
|
||||
api,
|
||||
constants,
|
||||
extendOptions,
|
||||
classes,
|
||||
vm,
|
||||
parent,
|
||||
$bus
|
||||
})
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
// 触发生命周期
|
||||
useVueLifeHooks($bus)
|
||||
|
||||
Object.keys(setupResult).forEach((key) => {
|
||||
vm[key] = setupResult[key]
|
||||
})
|
||||
|
||||
return {
|
||||
...setupResult,
|
||||
_: {
|
||||
ref,
|
||||
vm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Svg, If, Component, Slot, For, Transition, vc, emitEvent, useVm, useFiber }
|
||||
|
||||
export * from './vue-hooks.js'
|
||||
export * from './vue-props.js'
|
||||
export * from './render-stack.js'
|
||||
export * from './vue-instance.js'
|
||||
export * from './hooks'
|
|
@ -0,0 +1,94 @@
|
|||
import { useState, useRef } from 'react'
|
||||
import { computed } from './vue-hooks'
|
||||
|
||||
// 响应式核心
|
||||
const reactiveMap = new WeakMap()
|
||||
const reactive = (staticObject, handler = {}, path = [], rootStaticObject = staticObject) => {
|
||||
reactiveMap.has(staticObject) ||
|
||||
reactiveMap.set(
|
||||
staticObject,
|
||||
new Proxy(staticObject, {
|
||||
get(target, property, receiver) {
|
||||
const targetVal = target[property]
|
||||
if (targetVal && targetVal['v-hooks-type'] === computed) {
|
||||
return targetVal.value
|
||||
}
|
||||
|
||||
const _path = [...path, property]
|
||||
const res = typeof targetVal === 'object' ? reactive(targetVal, handler, _path, rootStaticObject) : targetVal
|
||||
|
||||
// 监听访问
|
||||
handler.get &&
|
||||
handler.get({
|
||||
result: res,
|
||||
root: rootStaticObject,
|
||||
path: _path,
|
||||
target,
|
||||
property,
|
||||
receiver
|
||||
})
|
||||
|
||||
return res
|
||||
},
|
||||
set(target, property, value, receiver) {
|
||||
const targetVal = target[property]
|
||||
if (targetVal && targetVal['v-hooks-type'] === computed) {
|
||||
targetVal.value = value
|
||||
return true
|
||||
}
|
||||
|
||||
const _path = [...path, property]
|
||||
|
||||
// 监听修改
|
||||
handler.set &&
|
||||
handler.set({
|
||||
target,
|
||||
property,
|
||||
receiver,
|
||||
root: rootStaticObject,
|
||||
path: _path,
|
||||
newVal: value,
|
||||
oldVal: target[property]
|
||||
})
|
||||
|
||||
target[property] = value
|
||||
return true
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return reactiveMap.get(staticObject)
|
||||
}
|
||||
|
||||
export const useReload = () => {
|
||||
const setReload = useState(0)[1]
|
||||
return () => setReload((pre) => pre + 1)
|
||||
}
|
||||
|
||||
const isObject = (val) => val !== null && typeof val === 'object'
|
||||
|
||||
// 用于从 hooks 链表中查找 reactive 生成的 state
|
||||
export function Reactive(obj) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
this[key] = obj[key]
|
||||
})
|
||||
}
|
||||
|
||||
export const useReactive = (initalObject) => {
|
||||
if (!isObject(initalObject)) {
|
||||
return initalObject
|
||||
}
|
||||
const memoried = useRef()
|
||||
const proxy = useRef()
|
||||
const reload = useReload()
|
||||
|
||||
if (!memoried.current && !proxy.current) {
|
||||
memoried.current = new Reactive(initalObject)
|
||||
proxy.current = reactive(memoried.current, {
|
||||
set() {
|
||||
reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
return proxy.current
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
const renderStack = []
|
||||
|
||||
export const getParent = () => renderStack[renderStack.length - 1] || {}
|
||||
|
||||
export const getRoot = () => renderStack[0] || {}
|
||||
|
||||
export const EnterStack = (props) => {
|
||||
renderStack.push(props)
|
||||
return ''
|
||||
}
|
||||
|
||||
export const LeaveStack = () => {
|
||||
renderStack.pop()
|
||||
return ''
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// todo: 一个方法去拿到 props 身上的事件,以 on 为前缀
|
||||
const reactEventPrefix = /^on[A-Z]/
|
||||
export function getEventByReactProps(props) {
|
||||
const $listeners = {}
|
||||
Object.keys(props)
|
||||
.filter((propName) => {
|
||||
return reactEventPrefix.test(propName) && typeof props[propName] === 'function'
|
||||
})
|
||||
.map((reactEvName) => {
|
||||
return {
|
||||
reactEvName,
|
||||
vueEvName: reactEvName.substr(2).toLowerCase()
|
||||
}
|
||||
})
|
||||
.forEach(({ reactEvName, vueEvName }) => {
|
||||
Object.assign($listeners, {
|
||||
[vueEvName]: props[reactEvName]
|
||||
})
|
||||
})
|
||||
return $listeners
|
||||
}
|
||||
export function getAttrsByReactProps(props) {
|
||||
const $attrs = {}
|
||||
Object.keys(props)
|
||||
.filter((propName) => {
|
||||
return !reactEventPrefix.test(propName) && !['children'].includes(propName)
|
||||
})
|
||||
.forEach((attr) => {
|
||||
$attrs[attr] = props[attr]
|
||||
})
|
||||
return $attrs
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import classNames from 'classnames'
|
||||
import { If } from './virtual-comp'
|
||||
|
||||
export const Svg = ({ name = 'Icon', component: Icon }) => {
|
||||
const funcObj = ({
|
||||
[name](props) {
|
||||
const className = classNames(
|
||||
'icon',
|
||||
'tiny-svg',
|
||||
props.className
|
||||
)
|
||||
const v_if = typeof props['v-if'] === 'boolean' ? props['v-if'] : true
|
||||
const defaultProps = { ...props }
|
||||
delete defaultProps['v-if']
|
||||
return (
|
||||
<If v-if={v_if}>
|
||||
<Icon {...defaultProps} className={className} />
|
||||
</If>
|
||||
)
|
||||
}
|
||||
})
|
||||
return funcObj[name]
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* filterAttrs 属性过滤函数
|
||||
* @param {object} attrs 由父组件传入,且没有被子组件声明为 props 的一些属性
|
||||
* @param {Array} filters 过滤数组,元素可以为字符串,也可以为正则表达式
|
||||
* @param {boolean} include 是否返回为被过滤的属性集合,如果为 false,filters 是过滤不要的属性
|
||||
* @returns {object} 过滤后的属性对象
|
||||
*/
|
||||
export const filterAttrs = (attrs, filters, include) => {
|
||||
const props = {}
|
||||
|
||||
for (let name in attrs) {
|
||||
const find = filters.some((r) => new RegExp(r).test(name))
|
||||
|
||||
if ((include && find) || (!include && !find)) {
|
||||
props[name] = attrs[name]
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
/**
|
||||
* event bus
|
||||
* $bus.on
|
||||
* $bus.off
|
||||
* $bus.emit
|
||||
*/
|
||||
|
||||
export const eventBus = () => {
|
||||
const $bus = {}
|
||||
|
||||
const on = (eventName, callback) => {
|
||||
if (!$bus[eventName]) {
|
||||
$bus[eventName] = []
|
||||
}
|
||||
|
||||
$bus[eventName].push(callback)
|
||||
}
|
||||
|
||||
const off = (eventName, callback) => {
|
||||
if (!$bus[eventName]) {
|
||||
return
|
||||
}
|
||||
|
||||
$bus[eventName] = $bus[eventName].filter((subscriber) => subscriber !== callback)
|
||||
}
|
||||
|
||||
const emit = (eventName, ...args) => {
|
||||
if (!$bus[eventName]) {
|
||||
return
|
||||
}
|
||||
|
||||
$bus[eventName].forEach((subscriber) => subscriber(...args))
|
||||
}
|
||||
|
||||
const once = (eventName, callback) => {
|
||||
const onceCallBack = (...args) => {
|
||||
callback(...args)
|
||||
off(eventName, onceCallBack)
|
||||
}
|
||||
on(eventName, onceCallBack)
|
||||
}
|
||||
|
||||
return {
|
||||
on,
|
||||
emit,
|
||||
off,
|
||||
once
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 vue 中 :class 的用法
|
||||
*/
|
||||
|
||||
export function VueClassName(className) {
|
||||
if (typeof className === 'string') {
|
||||
return className
|
||||
} else if (Array.isArray(className)) {
|
||||
return className.reduce((pre, cur, index) => {
|
||||
if (typeof cur === 'string') {
|
||||
return `${pre}${index === 0 ? '' : ' '}${cur}`
|
||||
} else {
|
||||
return `${pre}${index === 0 ? '' : ' '}${VueClassName(cur)}`
|
||||
}
|
||||
}, '')
|
||||
} else if (typeof className === 'object') {
|
||||
return Object.keys(className).reduce((pre, key, index) => {
|
||||
if (className[key]) {
|
||||
return `${pre}${index === 0 ? '' : ' '}${key}`
|
||||
} else {
|
||||
return pre
|
||||
}
|
||||
}, '')
|
||||
}
|
||||
}
|
||||
|
||||
export const vc = VueClassName
|
||||
|
||||
export const getElementCssClass = (classes = {}, key) => {
|
||||
if (typeof key === 'object') {
|
||||
const keys = Object.keys(key)
|
||||
let cls = ''
|
||||
keys.forEach((k) => {
|
||||
if (key[k] && classes[k]) cls += `${classes[k]} `
|
||||
})
|
||||
return cls
|
||||
} else {
|
||||
return classes[key] || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function getPropByPath(obj, path) {
|
||||
let tempObj = obj
|
||||
// 将a[b].c转换为a.b.c
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1')
|
||||
// 将.a.b转换为a.b
|
||||
path = path.replace(/^\./, '')
|
||||
|
||||
let keyArr = path.split('.')
|
||||
let len = keyArr.length
|
||||
|
||||
for (let i = 0; i < len - 1; i++) {
|
||||
let key = keyArr[i]
|
||||
if (key in tempObj) {
|
||||
tempObj = tempObj[key]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
return tempObj[keyArr[keyArr.length - 1]]
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
export function If(props) {
|
||||
if (props['v-if']) {
|
||||
return (props.children)
|
||||
}
|
||||
else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function defaultVIfAsTrue(props) {
|
||||
if (typeof props === 'object' && props.hasOwnProperty('v-if')) {
|
||||
return props['v-if'];
|
||||
}
|
||||
else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function Component(props) {
|
||||
const Is = props.is || (() => '')
|
||||
return <If v-if={defaultVIfAsTrue(props)}>
|
||||
<Is className={props.className} />
|
||||
</If>
|
||||
}
|
||||
|
||||
export function Slot(props) {
|
||||
const {
|
||||
name = 'default',
|
||||
slots = {},
|
||||
parent_children
|
||||
} = props
|
||||
|
||||
const EmptySlot = () => '';
|
||||
|
||||
const S = slots[name] || EmptySlot
|
||||
|
||||
return (<If v-if={defaultVIfAsTrue(props)}>
|
||||
<If v-if={name === 'default'}>
|
||||
{parent_children || props.children}
|
||||
</If>
|
||||
<If v-if={name !== 'default'}>
|
||||
<If v-if={S !== EmptySlot}>
|
||||
<S {...props} />
|
||||
</If>
|
||||
<If v-if={S === EmptySlot}>
|
||||
{props.children}
|
||||
</If>
|
||||
</If>
|
||||
</If>)
|
||||
}
|
||||
|
||||
export function For(props) {
|
||||
const {
|
||||
item: Item,
|
||||
list = []
|
||||
} = props
|
||||
|
||||
const listItems = list.map((item, index, list) => {
|
||||
return (<Item item={item} key={index} index={index} list={list} />)
|
||||
})
|
||||
|
||||
return (<If v-if={defaultVIfAsTrue(props)}>{listItems}</If>)
|
||||
}
|
||||
|
||||
export function Transition(props) {
|
||||
const {
|
||||
name
|
||||
} = props
|
||||
|
||||
// todo: improve tarnsiton comp
|
||||
return <If v-if={defaultVIfAsTrue(props)}>{props.children}</If>
|
||||
}
|
||||
|
||||
export const compWhiteList = [
|
||||
'If',
|
||||
'Component',
|
||||
'Slot',
|
||||
'For',
|
||||
'Transition'
|
||||
]
|
|
@ -0,0 +1,106 @@
|
|||
import { useFiber, getParentFiber, creatFiberCombine } from './fiber.js'
|
||||
import { getEventByReactProps, getAttrsByReactProps } from './resolve-props.js'
|
||||
import { Reactive } from './reactive.js'
|
||||
import { emit, on, off, once } from './event.js'
|
||||
|
||||
const vmProxy = {
|
||||
$parent: ({ fiber }) => {
|
||||
const parentFiber = getParentFiber(fiber)
|
||||
if (!parentFiber) return null
|
||||
return createVmProxy(creatFiberCombine(parentFiber))
|
||||
},
|
||||
$el: ({ fiber }) => fiber.child?.stateNode,
|
||||
$refs: ({ refs, fiber }) => createRefsProxy(refs, fiber.constructor),
|
||||
$children: ({ children }) => children.map((fiber) => createVmProxy(creatFiberCombine(getParentFiber(fiber)))),
|
||||
$listeners: ({ fiber }) => getEventByReactProps(fiber.memoizedProps),
|
||||
$attrs: ({ fiber }) => getAttrsByReactProps(fiber.memoizedProps),
|
||||
$slots: ({ fiber }) => fiber.memoizedProps.slots,
|
||||
$scopedSlots: ({ fiber }) => fiber.memoizedProps.slots,
|
||||
$options: ({ fiber }) => ({ componentName: fiber.type.name }),
|
||||
$constants: ({ fiber }) => fiber.memoizedProps._constants,
|
||||
$template: ({ fiber }) => fiber.memoizedProps.tiny_template,
|
||||
$renderless: ({ fiber }) => fiber.memoizedProps.tiny_renderless,
|
||||
$mode: () => 'pc',
|
||||
state: ({ fiber }) => findStateInHooks(fiber.memoizedState),
|
||||
$type: ({ fiber }) => fiber.type,
|
||||
$service: (_, vm) => vm.state?.$service,
|
||||
$emit: ({ fiber }) => emit(fiber.memoizedProps),
|
||||
$on: ({ fiber }) => on(fiber.memoizedProps),
|
||||
$once: ({ fiber }) => once(fiber.memoizedProps),
|
||||
$off: ({ fiber }) => off(fiber.memoizedProps),
|
||||
$set: () => (target, propName, value) => (target[propName] = value)
|
||||
}
|
||||
|
||||
const vmProxyMap = new WeakMap()
|
||||
function createVmProxy(fiberCombine) {
|
||||
if (!vmProxyMap.has(fiberCombine)) {
|
||||
vmProxyMap.set(
|
||||
fiberCombine,
|
||||
new Proxy(fiberCombine, {
|
||||
get(target, property, receiver) {
|
||||
if (!vmProxy[property]) {
|
||||
return target.fiber.memoizedProps[property]
|
||||
}
|
||||
return vmProxy[property](target, receiver)
|
||||
},
|
||||
set(target, property, value) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return vmProxyMap.get(fiberCombine)
|
||||
}
|
||||
|
||||
function createEmptyProxy() {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
return undefined
|
||||
},
|
||||
set() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function createRefsProxy(refs, FiberNode) {
|
||||
return new Proxy(refs, {
|
||||
get(target, property) {
|
||||
if (target[property] instanceof FiberNode) {
|
||||
return createVmProxy(creatFiberCombine(target[property]))
|
||||
} else {
|
||||
return target[property]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findStateInHooks(hookStart) {
|
||||
let curHook = hookStart
|
||||
// find state from hooks chain by Constructor Reactive
|
||||
while (curHook) {
|
||||
const refCurrent = curHook.memoizedState && curHook.memoizedState.current
|
||||
if (refCurrent instanceof Reactive) break
|
||||
curHook = curHook.next
|
||||
}
|
||||
return curHook && curHook.memoizedState && curHook.memoizedState.current
|
||||
}
|
||||
|
||||
export function useVm() {
|
||||
const { ref, current, parent } = useFiber()
|
||||
if (!ref.current) {
|
||||
return {
|
||||
ref,
|
||||
current: createEmptyProxy(),
|
||||
parent: createEmptyProxy()
|
||||
}
|
||||
}
|
||||
return {
|
||||
ref,
|
||||
current: current.fiber && createVmProxy(current),
|
||||
parent: parent.fiber && createVmProxy(parent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
import {
|
||||
// 响应式:核心
|
||||
ref,
|
||||
computed,
|
||||
reactive,
|
||||
readonly,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
// 响应式:工具
|
||||
isRef,
|
||||
unref,
|
||||
toRef,
|
||||
toValue,
|
||||
toRefs,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
// 响应式:进阶
|
||||
shallowRef,
|
||||
triggerRef,
|
||||
customRef,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
toRaw,
|
||||
markRaw,
|
||||
effectScope,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
// 通用
|
||||
nextTick
|
||||
} from '@vue/runtime-core'
|
||||
import { useExcuteOnce } from './hooks'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
// 通用
|
||||
const inject = () => {}
|
||||
const provide = () => {}
|
||||
|
||||
export function generateVueHooks({ $bus }) {
|
||||
const reload = () => $bus.emit('event:reload')
|
||||
|
||||
function toPageLoad(reactiveHook, reload) {
|
||||
return function (...args) {
|
||||
const result = reactiveHook(...args)
|
||||
nextTick(() => {
|
||||
watch(
|
||||
result,
|
||||
() => {
|
||||
typeof reload === 'function' && reload()
|
||||
},
|
||||
{
|
||||
flush: 'sync'
|
||||
}
|
||||
)
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 响应式:核心
|
||||
ref: toPageLoad(ref, reload),
|
||||
computed: toPageLoad(computed, reload),
|
||||
reactive: toPageLoad(reactive, reload),
|
||||
readonly,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
watch,
|
||||
// 响应式:工具
|
||||
isRef,
|
||||
unref,
|
||||
toRef: toPageLoad(toRef, reload),
|
||||
toValue,
|
||||
toRefs,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
// 响应式:进阶
|
||||
shallowRef: toPageLoad(shallowRef, reload),
|
||||
triggerRef,
|
||||
customRef: toPageLoad(customRef, reload),
|
||||
shallowReactive: toPageLoad(shallowReactive, reload),
|
||||
shallowReadonly,
|
||||
toRaw,
|
||||
markRaw,
|
||||
effectScope,
|
||||
getCurrentScope,
|
||||
onScopeDispose,
|
||||
// 依赖注入
|
||||
inject,
|
||||
provide,
|
||||
// 生命周期函数
|
||||
onBeforeUnmount() {
|
||||
$bus.on('hook:onBeforeUnmount')
|
||||
},
|
||||
onMounted() {
|
||||
$bus.on('hook:onMounted')
|
||||
},
|
||||
onUpdated() {
|
||||
$bus.on('hook:onUpdated')
|
||||
},
|
||||
onUnmounted() {
|
||||
$bus.on('hook:onUnmounted')
|
||||
},
|
||||
onBeforeMount() {
|
||||
$bus.on('hook:onBeforeMount')
|
||||
},
|
||||
onBeforeUpdate() {
|
||||
$bus.on('hook:onBeforeUpdate')
|
||||
},
|
||||
onErrorCaptured() {
|
||||
$bus.on('hook:onErrorCaptured')
|
||||
},
|
||||
onRenderTracked() {
|
||||
$bus.on('hook:onRenderTracked')
|
||||
},
|
||||
onRenderTriggered() {
|
||||
$bus.on('hook:onRenderTriggered')
|
||||
},
|
||||
onActivated() {
|
||||
$bus.on('hook:onActivated')
|
||||
},
|
||||
onDeactivated() {
|
||||
$bus.on('hook:onDeactivated')
|
||||
},
|
||||
onServerPrefetch() {
|
||||
$bus.on('hook:onServerPrefetch')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在这里出发生命周期钩子
|
||||
export function useVueLifeHooks($bus) {
|
||||
$bus.emit('hook:onBeforeUpdate')
|
||||
nextTick(() => {
|
||||
$bus.emit('hook:onUpdated')
|
||||
})
|
||||
|
||||
useExcuteOnce(() => {
|
||||
$bus.emit('hook:onBeforeMount')
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
$bus.emit('hook:onMounted')
|
||||
|
||||
return () => {
|
||||
// 卸载
|
||||
$bus.emit('hook:onBeforeUnmount')
|
||||
nextTick(() => {
|
||||
$bus.emit('hook:onUnmounted')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
export * from '@vue/runtime-core'
|
|
@ -0,0 +1,85 @@
|
|||
import { useRef } from 'react'
|
||||
import { useExcuteOnce, useOnceResult } from './hooks'
|
||||
import { reactive, nextTick, watch, computed } from '@vue/runtime-core'
|
||||
import { getPropByPath } from './utils'
|
||||
import { getParent, getRoot } from './render-stack'
|
||||
import { getFiberByDom, traverseFiber } from './fiber'
|
||||
|
||||
const collectRefs = (rootEl, $children) => {
|
||||
const refs = {}
|
||||
if (!rootEl) return refs
|
||||
const rootFiber = getFiberByDom(rootEl)
|
||||
// 收集普通元素 ref
|
||||
traverseFiber(rootFiber, (fiber) => {
|
||||
if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v-ref')) {
|
||||
refs[fiber.stateNode.getAttribute('v-ref')] = fiber.stateNode
|
||||
}
|
||||
})
|
||||
// 收集组件元素 ref
|
||||
$children.forEach((child) => {
|
||||
if (child.$props['v-ref']) {
|
||||
refs[child.$props['v-ref']] = child
|
||||
}
|
||||
})
|
||||
return refs
|
||||
}
|
||||
|
||||
export function useCreateVueInstance({ $bus, props }) {
|
||||
const ref = useRef()
|
||||
const vm = useOnceResult(() =>
|
||||
reactive({
|
||||
$el: undefined,
|
||||
$options: props.$options || {},
|
||||
$props: props,
|
||||
$parent: getParent().vm || {},
|
||||
$root: getRoot().vm || {},
|
||||
$slots: props.slots,
|
||||
$scopedSlots: props.slots,
|
||||
$listeners: props.$listeners,
|
||||
$attrs: props.$attrs,
|
||||
// 通过 fiber 计算
|
||||
$children: [],
|
||||
$refs: computed(() => collectRefs(vm.$el, vm.$children)),
|
||||
// 方法
|
||||
$set: (target, property, value) => (target[property] = value),
|
||||
$delete: (target, property) => delete target[property],
|
||||
$watch: (expression, callback, options) => {
|
||||
if (typeof expression === 'string') {
|
||||
watch(() => getPropByPath(vm, expression), callback, options)
|
||||
} else if (typeof expression === 'function') {
|
||||
watch(expression, callback, options)
|
||||
}
|
||||
},
|
||||
$on: (event, callback) => $bus.on(event, callback),
|
||||
$once: (event, callback) => $bus.once(event, callback),
|
||||
$off: (event, callback) => $bus.off(event, callback),
|
||||
$emit: (event, ...args) => $bus.emit(event, ...args),
|
||||
$forceUpdate: () => $bus.emit('event:reload'),
|
||||
$nextTick: nextTick,
|
||||
$destroy: () => {},
|
||||
$mount: () => {}
|
||||
})
|
||||
)
|
||||
|
||||
useExcuteOnce(() => {
|
||||
const { $listeners } = props
|
||||
Object.keys($listeners).forEach((eventName) => {
|
||||
$bus.on(eventName, $listeners[eventName])
|
||||
})
|
||||
|
||||
// 给父的 $children 里 push 当前的 vm
|
||||
const parent = vm.$parent
|
||||
if (Array.isArray(parent.$children)) {
|
||||
parent.$children.push(vm)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
vm.$el = ref.current
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
ref,
|
||||
vm
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
export function defineVueProps(propsOptions, props) {
|
||||
const $props = {}
|
||||
const $attrs = {}
|
||||
const $listeners = {}
|
||||
const reactEventPrefix = /^on[A-Z]/
|
||||
|
||||
const propsArray = Array.isArray(propsOptions) ? propsOptions : Object.keys(propsOptions)
|
||||
Object.keys(props).forEach((key) => {
|
||||
if (propsArray.includes(key)) {
|
||||
$props[key] = props[key]
|
||||
} else {
|
||||
if (reactEventPrefix.test(key)) {
|
||||
$listeners[key.substr(2).toLowerCase()] = props[key]
|
||||
} else {
|
||||
$attrs[key] = props[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof propsOptions === 'object') {
|
||||
Object.keys(propsOptions)
|
||||
.filter((key) => !$props[key])
|
||||
.forEach((key) => {
|
||||
const options = propsOptions[key]
|
||||
const defaultValue = typeof options.default === 'function' ? options.default() : options.default
|
||||
defaultValue !== undefined && ($props[key] = defaultValue)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
$props,
|
||||
$attrs,
|
||||
$listeners
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue