single block editor
This commit is contained in:
parent
be59f5832a
commit
91b9c8018b
|
|
@ -10,6 +10,10 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-extensions-color": "^4.23.0",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@uiw/react-json-view": "1.12.0",
|
||||
"localforage": "^1.10.0",
|
||||
"match-sorter": "^6.3.4",
|
||||
"react": "^18.2.0",
|
||||
|
|
@ -18,9 +22,12 @@
|
|||
"react-transition-group": "^4.4.5",
|
||||
"sort-by": "^1.2.0",
|
||||
"sucrase": "^3.31.0",
|
||||
"terser": "^5.31.3",
|
||||
"use-editable": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
|
||||
.preview-box > div *:not(div),
|
||||
.preview-box > div div *:not(div) {
|
||||
cursor: default;
|
||||
/* cursor: initial; */
|
||||
}
|
||||
|
|
@ -5,32 +5,42 @@ import evalCode from "../../utils/transpile/evalCode";
|
|||
import transform from "../../utils/transpile/transform";
|
||||
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
|
||||
import errorBoundary from "../../utils/transpile/errorBoundary";
|
||||
import "./style.css"
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
import "./style.css";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
import minifyCode from "../../utils/minify";
|
||||
// import ReactDOM from "react-dom/client";
|
||||
|
||||
export const App = () => {
|
||||
// 定义成一个数组便于下面操作,实际上只有一个元素
|
||||
const [results, setResults] = useState<ComponentType[]>([]);
|
||||
// 定义成一个数组便于下面操作,实际上只有一个元素
|
||||
const [results, setResults] = useState<ComponentType[]>([]);
|
||||
|
||||
const errorCallBack = (error: Error) => {
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const transforms: Transform[] = ["jsx", "typescript", "imports"];
|
||||
|
||||
const render = (element: ComponentType) => {
|
||||
if (typeof element === "undefined") {
|
||||
errorCallBack(new SyntaxError("`render` must be called with valid JSX."));
|
||||
} else {
|
||||
setResults([errorBoundary(element, errorCallBack)]);
|
||||
}
|
||||
};
|
||||
|
||||
// 开发环境下,useEffect会执行两次,模拟装载和卸载组件,生产环境没事。
|
||||
useEffect(() => {
|
||||
const allCpns = functionBlockList.map(item => item.code).join("\n");
|
||||
const allPage = functionBlockList.map((item,index) => `<${item.cpnName} { ...props[${index}] }/>`).join("\n");
|
||||
const allCode = `
|
||||
const errorCallBack = (error: Error) => {
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
const transforms: Transform[] = ["jsx", "typescript", "imports"];
|
||||
|
||||
const render = (element: ComponentType) => {
|
||||
const Component = errorBoundary(element, errorCallBack);
|
||||
if (typeof element === "undefined") {
|
||||
errorCallBack(new SyntaxError("`render` must be called with valid JSX."));
|
||||
} else {
|
||||
setResults([Component]);
|
||||
}
|
||||
// ReactDOM.createRoot(document.querySelector(".test-app") as HTMLElement).render(
|
||||
// <React.StrictMode>
|
||||
// < Component />
|
||||
// </React.StrictMode>
|
||||
// );
|
||||
};
|
||||
|
||||
// 开发环境下,useEffect会执行两次,模拟装载和卸载组件,生产环境没事。
|
||||
useEffect(() => {
|
||||
const allCpns = functionBlockList.map((item) => item.code).join("\n");
|
||||
const allPage = functionBlockList
|
||||
.map((item, index) => `<${item.cpnName} { ...props[${index}] }/>`)
|
||||
.join("\n");
|
||||
const allCode = `
|
||||
${allCpns}
|
||||
|
||||
const ApplicationContext = () => {
|
||||
|
|
@ -43,29 +53,30 @@ export const App = () => {
|
|||
|
||||
render(ApplicationContext)
|
||||
`;
|
||||
|
||||
const propsList = functionBlockList.map(item => item.props);
|
||||
|
||||
const allTransformed = transform({ transforms })(allCode);
|
||||
const propsList = functionBlockList.map((item) => item.props);
|
||||
|
||||
setTimeout(() => {
|
||||
evalCode(allTransformed, { React, render, props: propsList });
|
||||
},1000)
|
||||
}, []);
|
||||
const allTransformed = transform({ transforms })(allCode);
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="application">
|
||||
<TransitionGroup>
|
||||
{results.map((Cpn, index) => (
|
||||
<CSSTransition key={index} timeout={500} classNames="fade"
|
||||
>
|
||||
<Cpn />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
{results.length === 0 && <div className="loading">loading...</div>}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
setTimeout(async () => {
|
||||
const minifiedCode = await minifyCode(allTransformed);
|
||||
|
||||
evalCode(minifiedCode, { React, render, props: propsList });
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="application">
|
||||
<TransitionGroup>
|
||||
{results.map((Cpn, index) => (
|
||||
<CSSTransition key={index} timeout={500} classNames="fade">
|
||||
<Cpn />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
{results.length === 0 && <div className="loading">loading...</div>}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,98 @@
|
|||
import { Extension } from '@codemirror/state';
|
||||
import { CompletionContext, autocompletion, Completion } from '@codemirror/autocomplete';
|
||||
|
||||
const colorMap = [
|
||||
{ label: "--primary-7", type: "variable", info: "(点击(click))" },
|
||||
{ label: "--primary-6", type: "variable", info: "(常规)" },
|
||||
{ label: "--primary-5", type: "variable", info: "(悬浮(hover))" },
|
||||
{ label: "--primary-4", type: "variable", info: "(特殊场景)" },
|
||||
{ label: "--primary-3", type: "variable", info: "(一般禁用)" },
|
||||
{ label: "--primary-2", type: "variable", info: "(文字禁用)" },
|
||||
{ label: "--primary-1", type: "variable", info: "(浅色/白底悬浮)" },
|
||||
{ label: "--success-7", type: "variable", info: "(点击(click))" },
|
||||
{ label: "--success-6", type: "variable", info: "(常规)" },
|
||||
{ label: "--success-5", type: "variable", info: "(悬浮(hover))" },
|
||||
{ label: "--success-4", type: "variable", info: "(特殊场景)" },
|
||||
{ label: "--success-3", type: "variable", info: "(一般禁用)" },
|
||||
{ label: "--success-2", type: "variable", info: "(文字禁用)" },
|
||||
{ label: "--success-1", type: "variable", info: "(浅色/白底悬浮)" },
|
||||
{ label: "--warning-7", type: "variable", info: "(点击(click))" },
|
||||
{ label: "--warning-6", type: "variable", info: "(常规)" },
|
||||
{ label: "--warning-5", type: "variable", info: "(悬浮(hover))" },
|
||||
{ label: "--warning-4", type: "variable", info: "(特殊场景)" },
|
||||
{ label: "--warning-3", type: "variable", info: "(一般禁用)" },
|
||||
{ label: "--warning-2", type: "variable", info: "(文字禁用)" },
|
||||
{ label: "--warning-1", type: "variable", info: "(浅色/白底悬浮)" },
|
||||
{ label: "--danger-7", type: "variable", info: "(点击(click))" },
|
||||
{ label: "--danger-6", type: "variable", info: "(常规)" },
|
||||
{ label: "--danger-5", type: "variable", info: "(悬浮(hover))" },
|
||||
{ label: "--danger-4", type: "variable", info: "(特殊场景)" },
|
||||
{ label: "--danger-3", type: "variable", info: "(一般禁用)" },
|
||||
{ label: "--danger-2", type: "variable", info: "(文字禁用)" },
|
||||
{ label: "--danger-1", type: "variable", info: "(浅色/白底悬浮)" },
|
||||
{ label: "--link-7", type: "variable", info: "(点击(click))" },
|
||||
{ label: "--link-6", type: "variable", info: "(常规)" },
|
||||
{ label: "--link-5", type: "variable", info: "(悬浮(hover))" },
|
||||
{ label: "--link-4", type: "variable", info: "(特殊场景)" },
|
||||
{ label: "--link-3", type: "variable", info: "(一般禁用)" },
|
||||
{ label: "--link-2", type: "variable", info: "(文字禁用)" },
|
||||
{ label: "--link-1", type: "variable", info: "(浅色/白底悬浮)" },
|
||||
{ label: "--data-1", type: "variable", info: "" },
|
||||
{ label: "--data-2", type: "variable", info: "" },
|
||||
{ label: "--data-3", type: "variable", info: "" },
|
||||
{ label: "--data-4", type: "variable", info: "" },
|
||||
{ label: "--data-5", type: "variable", info: "" },
|
||||
{ label: "--data-6", type: "variable", info: "" },
|
||||
{ label: "--data-7", type: "variable", info: "" },
|
||||
{ label: "--data-8", type: "variable", info: "" },
|
||||
{ label: "--data-9", type: "variable", info: "" },
|
||||
{ label: "--data-10", type: "variable", info: "" },
|
||||
{ label: "--data-11", type: "variable", info: "" },
|
||||
{ label: "--data-12", type: "variable", info: "" },
|
||||
{ label: "--data-13", type: "variable", info: "" },
|
||||
{ label: "--data-14", type: "variable", info: "" },
|
||||
{ label: "--data-15", type: "variable", info: "" },
|
||||
{ label: "--data-16", type: "variable", info: "" },
|
||||
{ label: "--data-17", type: "variable", info: "" },
|
||||
{ label: "--data-18", type: "variable", info: "" },
|
||||
{ label: "--data-19", type: "variable", info: "" },
|
||||
{ label: "--data-20", type: "variable", info: "" },
|
||||
{ label: "--color-border-1", type: "variable", info: "(浅色)" },
|
||||
{ label: "--color-border-2", type: "variable", info: "(一般)" },
|
||||
{ label: "--color-border-3", type: "variable", info: "(深/悬浮)" },
|
||||
{ label: "--color-border-4", type: "variable", info: "(重/按钮描边)" },
|
||||
{ label: "--color-fill-1", type: "variable", info: "(浅/禁用)" },
|
||||
{ label: "--color-fill-2", type: "variable", info: "(常规/白底悬浮)" },
|
||||
{ label: "--color-fill-3", type: "variable", info: "(深/灰底悬浮)" },
|
||||
{ label: "--color-fill-4", type: "variable", info: "(重/特殊场景)" },
|
||||
{ label: "--color-text-1", type: "variable", info: "(强调/正文标题)" },
|
||||
{ label: "--color-text-2", type: "variable", info: "(次强调/正文标题)" },
|
||||
{ label: "--color-text-3", type: "variable", info: "(次要信息)" },
|
||||
{ label: "--color-text-4", type: "variable", info: "(置灰信息)" },
|
||||
{ label: "--color-bg-1", type: "variable", info: "(整体背景色)" },
|
||||
{ label: "--color-bg-2", type: "variable", info: "(一级容器背景)" },
|
||||
{ label: "--color-bg-3", type: "variable", info: "(二级容器背景)" },
|
||||
{ label: "--color-bg-4", type: "variable", info: "(三级容器背景)" },
|
||||
{ label: "--color-bg-5", type: "variable", info: "(下拉弹出框、Tooltip 背景颜色)" },
|
||||
{ label: "--color-bg-white", type: "variable", info: "(白色背景)" }
|
||||
];
|
||||
|
||||
export function colorCompletions(data: Completion[] = []): Extension {
|
||||
return autocompletion({
|
||||
override: [
|
||||
(context: CompletionContext) => {
|
||||
let word = context.matchBefore(/--\w+/g);
|
||||
if (!word) return null;
|
||||
if (word && word.from == word.to && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: word?.from!,
|
||||
options: [...data],
|
||||
};
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export const plugin = colorCompletions(colorMap);
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
import React from "react";
|
||||
import CodeMirror, { basicSetup } from "@uiw/react-codemirror";
|
||||
import { loadLanguage, langs } from "@uiw/codemirror-extensions-langs";
|
||||
import LiveProvider from "../../components/Live/LiveProvider";
|
||||
import LivePreview from "../../components/Live/LivePreview";
|
||||
import LiveError from "../../components/Live/LiveError";
|
||||
import { color, colorView, colorTheme } from "@uiw/codemirror-extensions-color";
|
||||
import JsonViewEditor from "@uiw/react-json-view/editor";
|
||||
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
|
||||
import { plugin as colorCom } from "./completions";
|
||||
|
||||
loadLanguage("tsx");
|
||||
const extensions = [color, langs.tsx(), colorView(false), colorTheme, colorCom];
|
||||
|
||||
export const Creation = () => {
|
||||
// useEffect 拦截ctrls等操作,避免浏览器默认行为
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey && event.key === "s") {
|
||||
event.preventDefault();
|
||||
// 弹出已经保存的提示
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [componentName, setComponentName] = React.useState("Banner");
|
||||
|
||||
const [componentsProps, setComponentsProps] = React.useState({
|
||||
title: "AI智能建站助手",
|
||||
subTitle: "让我们帮助您快速构建和优化您的网站",
|
||||
btnText: "立即开始",
|
||||
});
|
||||
|
||||
const [newPropKey, setNewPropKey] = React.useState("");
|
||||
|
||||
const handleAddProp = () => {
|
||||
if (newPropKey.trim() !== "") {
|
||||
setComponentsProps((prevProps) => ({
|
||||
...prevProps,
|
||||
[newPropKey]: "",
|
||||
}));
|
||||
setNewPropKey("");
|
||||
}
|
||||
};
|
||||
|
||||
const [code, setCode] = React.useState(`
|
||||
const Banner = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
btnText: string
|
||||
}) => {
|
||||
function handleClick() {
|
||||
console.log("Hi there!");
|
||||
}
|
||||
return (
|
||||
<div className="bg-primary-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-primary text-5xl md:text-7xl font-extrabold mb-4">
|
||||
{props.title}
|
||||
</h1>
|
||||
<p className="text-primary text-xl md:text-2xl mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="font-bold py-3 px-6 rounded-full shadow-lg transition duration-300 ease-in-out"
|
||||
>
|
||||
{props.btnText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`);
|
||||
|
||||
const renderCode = (code: string) => {
|
||||
return `
|
||||
${code}
|
||||
render(${componentName});
|
||||
`;
|
||||
};
|
||||
|
||||
const onChange = React.useCallback((value: string) => {
|
||||
setCode(value);
|
||||
}, []);
|
||||
|
||||
const handleComponentNameChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setComponentName(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<div className="p-4">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold text-lg border-primary-1">
|
||||
组件名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={componentName}
|
||||
onChange={handleComponentNameChange}
|
||||
className="border border-text-1 rounded p-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
<LiveProvider code={renderCode(code)} noInline>
|
||||
<div className="h-full grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
|
||||
<div className="border rounded p-2">
|
||||
<CodeMirror
|
||||
value={code}
|
||||
height="100%"
|
||||
extensions={[
|
||||
...extensions,
|
||||
basicSetup({
|
||||
autocompletion: true,
|
||||
lintKeymap: true,
|
||||
}),
|
||||
]}
|
||||
onChange={onChange}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="border rounded p-2">
|
||||
<LivePreview {...componentsProps} />
|
||||
</div>
|
||||
</div>
|
||||
<LiveError className="text-danger-6 mt-2 p-2 rounded" />
|
||||
</LiveProvider>
|
||||
<div className="mt-4 p-4 border rounded shadow w-2/4">
|
||||
<h2 className="font-bold text-lg mb-2">Props 编辑器</h2>
|
||||
<div className="props-edit">
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 font-bold">新属性名</label>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={newPropKey}
|
||||
onChange={(e) => setNewPropKey(e.target.value)}
|
||||
className="border border-text-1 rounded p-2 flex-grow"
|
||||
/>
|
||||
<button onClick={handleAddProp} className="p-2 rounded ml-2">
|
||||
添加
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<JsonViewEditor
|
||||
value={componentsProps}
|
||||
keyName="props"
|
||||
style={
|
||||
{
|
||||
"--w-rjv-background-color": "#ffffff",
|
||||
"--w-rjv-border-left": "1px dashed #ebebeb",
|
||||
"--w-rjv-update-color": "#ff6ffd",
|
||||
} as any
|
||||
}
|
||||
onEdit={(opts) => {
|
||||
const updateNestedProp = (
|
||||
obj: Record<string, any>,
|
||||
path: string[],
|
||||
value: unknown
|
||||
) => {
|
||||
if (path.length === 1) {
|
||||
obj[path[0]] = value;
|
||||
} else {
|
||||
if (!obj[path[0]]) {
|
||||
obj[path[0]] = {};
|
||||
}
|
||||
updateNestedProp(obj[path[0]], path.slice(1), value);
|
||||
}
|
||||
};
|
||||
const updatedProps = { ...componentsProps };
|
||||
updateNestedProp(
|
||||
updatedProps,
|
||||
(opts as any).namespace,
|
||||
opts.value
|
||||
);
|
||||
setComponentsProps(updatedProps);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,7 +7,7 @@ import { useState } from "react";
|
|||
import { functionBlockList } from "../../data/functionBlocks";
|
||||
import ClickContextProvider from "../../components/Selected/ClickContext";
|
||||
|
||||
const code = (func: string,cpnName:string) => `
|
||||
const code = (func: string, cpnName: string) => `
|
||||
${func}
|
||||
render(${cpnName})
|
||||
`;
|
||||
|
|
@ -112,7 +112,11 @@ export const Editor = () => {
|
|||
)}
|
||||
{functionBlockList.map((func, index) => {
|
||||
return (
|
||||
<LiveProvider code={code(func.code, func.cpnName)} noInline key={index}>
|
||||
<LiveProvider
|
||||
code={code(func.code, func.cpnName)}
|
||||
noInline
|
||||
key={index}
|
||||
>
|
||||
<ClickContextProvider
|
||||
meun={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import {
|
||||
createBrowserRouter,
|
||||
} from "react-router-dom";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import { Editor } from "../pages/editor";
|
||||
import { App } from "../pages/app";
|
||||
import { Creation } from "../pages/creation";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <div>Hello world!</div>,
|
||||
},
|
||||
{
|
||||
path: "/editor",
|
||||
element: <Editor/>,
|
||||
},
|
||||
{
|
||||
path: "/app",
|
||||
element: <App/>,
|
||||
}
|
||||
{
|
||||
path: "/",
|
||||
element: <div>Hello world!</div>,
|
||||
},
|
||||
{
|
||||
path: "/editor",
|
||||
element: <Editor />,
|
||||
},
|
||||
{
|
||||
path: "/app",
|
||||
element: <App />,
|
||||
},
|
||||
{
|
||||
path: "/create",
|
||||
element: <Creation />,
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { minify } from "terser";
|
||||
|
||||
export default async function minifyCode(code: string) {
|
||||
const replaced = code
|
||||
.replace(RegExp(`React.createElement`, "g"), `rc`)
|
||||
.replace(RegExp(`React.Fragment`, "g"), `rf`)
|
||||
.replace(RegExp(`React.useState`, "g"), `rs`)
|
||||
.replace(RegExp(`React.useEffect`, "g"), `re`)
|
||||
.replace(RegExp(`React.Component`, "g"), `rcp`)
|
||||
.replace(RegExp(`React.createContext`, "g"), `rcc`)
|
||||
.replace(RegExp(`React.useContext`, "g"), `rcu`)
|
||||
.replace(RegExp(`React.useReducer`, "g"), `rd`)
|
||||
.replace(RegExp(`React.memo`, "g"), `rm`)
|
||||
.replace(
|
||||
`"use strict";`,
|
||||
`"use strict"; const rc=React.createElement,rf=React.Fragment,rs=React.useState,re=React.useEffect,rcp=React.Component,rcc=React.createContext,rcu=React.useContext,rd=React.useReducer,rm=React.memo;`
|
||||
);
|
||||
|
||||
const { code: minifiedCode } = await minify(replaced, {
|
||||
compress: {
|
||||
passes: 2,
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
arguments: true,
|
||||
},
|
||||
mangle: {
|
||||
toplevel: true,
|
||||
},
|
||||
});
|
||||
return minifiedCode || "";
|
||||
}
|
||||
Loading…
Reference in New Issue