Complete main UI #37
46
package-lock.json
generated
46
package-lock.json
generated
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@marsidev/react-turnstile": "^0.4.0",
|
"@marsidev/react-turnstile": "^0.4.0",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@typescript/ata": "^0.9.4",
|
||||||
"antd": "^5.12.1",
|
"antd": "^5.12.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"match-sorter": "^6.3.1",
|
"match-sorter": "^6.3.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"monaco-editor": "^0.45.0",
|
"monaco-editor": "^0.45.0",
|
||||||
|
"monaco-jsx-syntax-highlight": "^1.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.20.1",
|
"react-router": "^6.20.1",
|
||||||
@@ -1243,6 +1246,30 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@monaco-editor/loader": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==",
|
||||||
|
"dependencies": {
|
||||||
|
"state-local": "^1.0.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"monaco-editor": ">= 0.21.0 < 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@monaco-editor/react": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@monaco-editor/loader": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"monaco-editor": ">= 0.25.0 < 1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -2196,6 +2223,14 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript/ata": {
|
||||||
|
"version": "0.9.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@typescript/ata/-/ata-0.9.4.tgz",
|
||||||
|
"integrity": "sha512-PaJ16WouPV/SaA+c0tnOKIqYq24+m93ipl/e0Dkxuianer+ibc5b0/6ZgfCFF8J7QEp57dySMSP9nWOFaCfJnw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||||
@@ -5275,6 +5310,11 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz",
|
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.45.0.tgz",
|
||||||
"integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
|
"integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/monaco-jsx-syntax-highlight": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/monaco-jsx-syntax-highlight/-/monaco-jsx-syntax-highlight-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-sKzfmNMxLp3Dcwndz0F/EdP3rqe+Z6FbDPazj+/ByNbqoXWkzgb/ocat8h3vpiTwPPiDtSHidT0BFMvLkMtG8A=="
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
|
||||||
@@ -7127,6 +7167,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/state-local": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="
|
||||||
|
},
|
||||||
"node_modules/string-convert": {
|
"node_modules/string-convert": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
|
||||||
@@ -7705,7 +7750,6 @@
|
|||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@marsidev/react-turnstile": "^0.4.0",
|
"@marsidev/react-turnstile": "^0.4.0",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@typescript/ata": "^0.9.4",
|
||||||
"antd": "^5.12.1",
|
"antd": "^5.12.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"match-sorter": "^6.3.1",
|
"match-sorter": "^6.3.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"monaco-editor": "^0.45.0",
|
"monaco-editor": "^0.45.0",
|
||||||
|
"monaco-jsx-syntax-highlight": "^1.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.20.1",
|
"react-router": "^6.20.1",
|
||||||
|
|||||||
83
src/components/ReactPlayground/CodeEditor/Editor/ata.ts
Normal file
83
src/components/ReactPlayground/CodeEditor/Editor/ata.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { ATABootstrapConfig, setupTypeAcquisition } from '@typescript/ata'
|
||||||
|
|
||||||
|
type DelegateListener = Required<{
|
||||||
|
[k in keyof ATABootstrapConfig['delegate']]: Set<NonNullable<ATABootstrapConfig['delegate'][k]>>
|
||||||
|
}>
|
||||||
|
|
||||||
|
const createDelegate = (): DelegateListener => {
|
||||||
|
return {
|
||||||
|
receivedFile: new Set(),
|
||||||
|
progress: new Set(),
|
||||||
|
errorMessage: new Set(),
|
||||||
|
finished: new Set(),
|
||||||
|
started: new Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const delegateListener = createDelegate()
|
||||||
|
|
||||||
|
type InferSet<T> = T extends Set<infer U> ? U : never
|
||||||
|
|
||||||
|
export const createATA = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const ts = await import('https://esm.sh/typescript@5.3.3')
|
||||||
|
const ata = setupTypeAcquisition({
|
||||||
|
projectName: 'monaco-ts',
|
||||||
|
typescript: ts,
|
||||||
|
logger: console,
|
||||||
|
fetcher: (input, init) => {
|
||||||
|
let result: any
|
||||||
|
try {
|
||||||
|
result = fetch(input, init)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
delegate: {
|
||||||
|
receivedFile: (code, path) => {
|
||||||
|
delegateListener.receivedFile.forEach((fn) => fn(code, path))
|
||||||
|
},
|
||||||
|
progress: (downloaded, estimatedTotal) => {
|
||||||
|
delegateListener.progress.forEach((fn) => fn(downloaded, estimatedTotal))
|
||||||
|
},
|
||||||
|
started: () => {
|
||||||
|
delegateListener.started.forEach((fn) => fn())
|
||||||
|
},
|
||||||
|
finished: (files) => {
|
||||||
|
delegateListener.finished.forEach((fn) => fn(files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const acquireType = (code: string) => ata(code)
|
||||||
|
|
||||||
|
const addListener = <T extends keyof DelegateListener>(
|
||||||
|
event: T,
|
||||||
|
handler: InferSet<DelegateListener[T]>
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
delegateListener[event].add(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeListener = <T extends keyof DelegateListener>(
|
||||||
|
event: T,
|
||||||
|
handler: InferSet<DelegateListener[T]>
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
delegateListener[event].delete(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispose = () => {
|
||||||
|
for (const key in delegateListener) {
|
||||||
|
delegateListener[key as keyof DelegateListener].clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
acquireType,
|
||||||
|
addListener,
|
||||||
|
removeListener,
|
||||||
|
dispose
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/components/ReactPlayground/CodeEditor/Editor/editor.scss
Normal file
43
src/components/ReactPlayground/CodeEditor/Editor/editor.scss
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.monaco-editor-light {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--border);
|
||||||
|
|
||||||
|
.jsx-tag-angle-bracket {
|
||||||
|
color: #800000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-text {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-tag-name {
|
||||||
|
color: #800000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-tag-attribute-key {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor-vs-dark {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--border);
|
||||||
|
|
||||||
|
.jsx-tag-angle-bracket {
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-text {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-tag-name {
|
||||||
|
color: #569cd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsx-tag-attribute-key {
|
||||||
|
color: #9cdcfe;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/components/ReactPlayground/CodeEditor/Editor/hooks.ts
Normal file
72
src/components/ReactPlayground/CodeEditor/Editor/hooks.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { editor, IPosition, Selection } from 'monaco-editor'
|
||||||
|
import ScrollType = editor.ScrollType
|
||||||
|
import { Monaco } from '@monaco-editor/react'
|
||||||
|
import { getWorker, MonacoJsxSyntaxHighlight } from 'monaco-jsx-syntax-highlight'
|
||||||
|
import { createATA } from '@/components/ReactPlayground/CodeEditor/Editor/ata.ts'
|
||||||
|
|
||||||
|
export const useEditor = () => {
|
||||||
|
const doOpenEditor = (
|
||||||
|
editor: editor.IStandaloneCodeEditor,
|
||||||
|
input: { options: { selection: Selection } }
|
||||||
|
) => {
|
||||||
|
const selection = input.options ? input.options.selection : null
|
||||||
|
if (selection) {
|
||||||
|
if (
|
||||||
|
typeof selection.endLineNumber === 'number' &&
|
||||||
|
typeof selection.endColumn === 'number'
|
||||||
|
) {
|
||||||
|
editor.setSelection(selection)
|
||||||
|
editor.revealRangeInCenter(selection, ScrollType.Immediate)
|
||||||
|
} else {
|
||||||
|
const position: IPosition = {
|
||||||
|
lineNumber: selection.startLineNumber,
|
||||||
|
column: selection.startColumn
|
||||||
|
}
|
||||||
|
editor.setPosition(position)
|
||||||
|
editor.revealPositionInCenter(position, ScrollType.Immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadJsxSyntaxHighlight = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
|
const monacoJsxSyntaxHighlight = new MonacoJsxSyntaxHighlight(getWorker(), monaco)
|
||||||
|
const { highlighter, dispose } = monacoJsxSyntaxHighlight.highlighterBuilder({ editor })
|
||||||
|
|
||||||
|
editor.onDidChangeModelContent(() => {
|
||||||
|
highlighter()
|
||||||
|
})
|
||||||
|
highlighter()
|
||||||
|
|
||||||
|
return { highlighter, dispose }
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoLoadExtraLib = async (
|
||||||
|
editor: editor.IStandaloneCodeEditor,
|
||||||
|
monaco: Monaco,
|
||||||
|
defaultValue: string,
|
||||||
|
onWatch: any
|
||||||
|
) => {
|
||||||
|
const typeHelper = await createATA()
|
||||||
|
|
||||||
|
onWatch(typeHelper)
|
||||||
|
|
||||||
|
editor.onDidChangeModelContent(() => {
|
||||||
|
typeHelper.acquireType(editor.getValue())
|
||||||
|
})
|
||||||
|
|
||||||
|
const addLibraryToRuntime = (code: string, path: string) => {
|
||||||
|
monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file://${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeHelper.addListener('receivedFile', addLibraryToRuntime)
|
||||||
|
typeHelper.acquireType(defaultValue)
|
||||||
|
|
||||||
|
return typeHelper
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
doOpenEditor,
|
||||||
|
loadJsxSyntaxHighlight,
|
||||||
|
autoLoadExtraLib
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/components/ReactPlayground/CodeEditor/Editor/index.tsx
Normal file
90
src/components/ReactPlayground/CodeEditor/Editor/index.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { editor, Selection } from 'monaco-editor'
|
||||||
|
import MonacoEditor, { Monaco } from '@monaco-editor/react'
|
||||||
|
import '@/components/ReactPlayground/CodeEditor/Editor/editor.scss'
|
||||||
|
import { IEditorOptions, IFile, IFiles, ITheme } from '@/components/ReactPlayground/shared'
|
||||||
|
import { MonacoEditorConfig } from '@/components/ReactPlayground/CodeEditor/Editor/monacoConfig'
|
||||||
|
import { fileNameToLanguage } from '@/components/ReactPlayground/utils'
|
||||||
|
import { useEditor } from '@/components/ReactPlayground/CodeEditor/Editor/hooks'
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
file: IFile
|
||||||
|
onChange?: (code: string | undefined) => void
|
||||||
|
options?: IEditorOptions
|
||||||
|
theme?: ITheme
|
||||||
|
files?: IFiles
|
||||||
|
onJumpFile?: (fileName: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Editor: React.FC<EditorProps> = ({ file, files, theme, onChange, options, onJumpFile }) => {
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor>()
|
||||||
|
const { doOpenEditor, loadJsxSyntaxHighlight } = useEditor()
|
||||||
|
const jsxSyntaxHighlightRef = useRef<{
|
||||||
|
highlighter: (code?: string | undefined) => void
|
||||||
|
dispose: () => void
|
||||||
|
}>({
|
||||||
|
highlighter: () => undefined,
|
||||||
|
dispose: () => undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOnEditorDidMount = (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
|
editorRef.current = editor
|
||||||
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
||||||
|
void editor.getAction('editor.action.formatDocument')?.run()
|
||||||
|
})
|
||||||
|
|
||||||
|
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||||
|
jsx: monaco.languages.typescript.JsxEmit.Preserve,
|
||||||
|
esModuleInterop: true
|
||||||
|
})
|
||||||
|
|
||||||
|
files &&
|
||||||
|
Object.entries(files).forEach(([key]) => {
|
||||||
|
if (!monaco.editor.getModel(monaco.Uri.parse(`file:///${key}`))) {
|
||||||
|
monaco.editor.createModel(
|
||||||
|
files[key].value,
|
||||||
|
fileNameToLanguage(key),
|
||||||
|
monaco.Uri.parse(`file:///${key}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
editor['_codeEditorService'].doOpenEditor = function (
|
||||||
|
editor: editor.IStandaloneCodeEditor,
|
||||||
|
input: { options: { selection: Selection }; resource: { path: string } }
|
||||||
|
) {
|
||||||
|
const path = input.resource.path
|
||||||
|
if (!path.startsWith('/node_modules/')) {
|
||||||
|
onJumpFile?.(path.replace('/', ''))
|
||||||
|
doOpenEditor(editor, input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsxSyntaxHighlightRef.current = loadJsxSyntaxHighlight(editor, monaco)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current?.focus()
|
||||||
|
jsxSyntaxHighlightRef?.current?.highlighter?.()
|
||||||
|
}, [file.name])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MonacoEditor
|
||||||
|
theme={theme}
|
||||||
|
path={file.name}
|
||||||
|
className={`monaco-editor-${theme ?? 'light'}`}
|
||||||
|
language={file.language}
|
||||||
|
value={file.value}
|
||||||
|
onChange={onChange}
|
||||||
|
onMount={handleOnEditorDidMount}
|
||||||
|
options={{ ...MonacoEditorConfig, ...options, theme: undefined }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Editor
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { editor } from 'monaco-editor'
|
||||||
|
|
||||||
|
export const MonacoEditorConfig: editor.IStandaloneEditorConstructionOptions = {
|
||||||
|
automaticLayout: true,
|
||||||
|
cursorBlinking: 'smooth',
|
||||||
|
fontLigatures: true,
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
fontSize: 14,
|
||||||
|
showDeprecated: true,
|
||||||
|
showUnused: true,
|
||||||
|
showFoldingControls: 'mouseover',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
inlineSuggest: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
fixedOverflowWidgets: true,
|
||||||
|
smoothScrolling: true,
|
||||||
|
smartSelect: {
|
||||||
|
selectSubwords: true,
|
||||||
|
selectLeadingAndTrailingWhitespace: true,
|
||||||
|
},
|
||||||
|
tabSize: 2,
|
||||||
|
overviewRulerBorder: false, // 不要滚动条的边框
|
||||||
|
// 滚动条设置
|
||||||
|
scrollbar: {
|
||||||
|
verticalScrollbarSize: 6, // 竖滚动条
|
||||||
|
horizontalScrollbarSize: 6, // 横滚动条
|
||||||
|
},
|
||||||
|
// lineNumbers: 'off', // 隐藏控制行号
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { IPlayground } from '@/components/ReactPlayground/shared.ts'
|
import { IPlayground } from '@/components/ReactPlayground/shared.ts'
|
||||||
import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx'
|
import { PlaygroundContext } from '@/components/ReactPlayground/Provider.tsx'
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
getCustomActiveFile,
|
getCustomActiveFile,
|
||||||
getMergedCustomFiles,
|
getMergedCustomFiles,
|
||||||
getPlaygroundTheme
|
getPlaygroundTheme
|
||||||
} from '@/components/ReactPlayground/Utils.ts'
|
} from '@/components/ReactPlayground/utils.ts'
|
||||||
|
|
||||||
const defaultCodeSandboxOptions = {
|
const defaultCodeSandboxOptions = {
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
@@ -67,7 +68,8 @@ const Playground: React.FC<IPlayground> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return files[ENTRY_FILE_NAME] ? <></> : undefined
|
return files[ENTRY_FILE_NAME] ? <div></div> : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Playground
|
export default Playground
|
||||||
|
*/
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts'
|
import { IFiles, IPlaygroundContext, ITheme } from '@/components/ReactPlayground/shared.ts'
|
||||||
import { MAIN_FILE_NAME } from '@/components/files.ts'
|
import { MAIN_FILE_NAME } from '@/components/ReactPlayground/files.ts'
|
||||||
import {
|
import {
|
||||||
fileNameToLanguage,
|
fileNameToLanguage,
|
||||||
setPlaygroundTheme,
|
setPlaygroundTheme,
|
||||||
strToBase64
|
strToBase64
|
||||||
} from '@/components/ReactPlayground/Utils.ts'
|
} from '@/components/ReactPlayground/utils.ts'
|
||||||
|
|
||||||
const initialContext: Partial<IPlaygroundContext> = {
|
const initialContext: Partial<IPlaygroundContext> = {
|
||||||
selectedFileName: MAIN_FILE_NAME
|
selectedFileName: MAIN_FILE_NAME
|
||||||
|
|||||||
68
src/components/ReactPlayground/files.ts
Normal file
68
src/components/ReactPlayground/files.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import importMap from '@/components/ReactPlayground/template/import-map.json?raw'
|
||||||
|
import AppCss from '@/components/ReactPlayground/template/src/App.css?raw'
|
||||||
|
import App from '@/components/ReactPlayground/template/src/App.tsx?raw'
|
||||||
|
import main from '@/components/ReactPlayground/template/src/main.tsx?raw'
|
||||||
|
import { IFiles } from '@/components/ReactPlayground/shared'
|
||||||
|
import { base64ToStr } from '@/components/ReactPlayground/utils.ts'
|
||||||
|
|
||||||
|
export const MAIN_FILE_NAME = 'App.tsx'
|
||||||
|
export const IMPORT_MAP_FILE_NAME = 'import-map.json'
|
||||||
|
export const ENTRY_FILE_NAME = 'main.tsx'
|
||||||
|
|
||||||
|
const fileNameToLanguage = (name: string) => {
|
||||||
|
const suffix = name.split('.').pop() || ''
|
||||||
|
if (['js', 'jsx'].includes(suffix)) return 'javascript'
|
||||||
|
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
|
||||||
|
if (['json'].includes(suffix)) return 'json'
|
||||||
|
if (['css'].includes(suffix)) return 'css'
|
||||||
|
return 'javascript'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilesFromUrl = () => {
|
||||||
|
let files: IFiles | undefined
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const hash = window.location.hash
|
||||||
|
if (hash) files = JSON.parse(base64ToStr(hash?.split('#')[1]))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initFiles: IFiles = getFilesFromUrl() || {
|
||||||
|
[ENTRY_FILE_NAME]: {
|
||||||
|
name: ENTRY_FILE_NAME,
|
||||||
|
language: fileNameToLanguage(ENTRY_FILE_NAME),
|
||||||
|
value: main
|
||||||
|
},
|
||||||
|
[MAIN_FILE_NAME]: {
|
||||||
|
name: MAIN_FILE_NAME,
|
||||||
|
language: fileNameToLanguage(MAIN_FILE_NAME),
|
||||||
|
value: App
|
||||||
|
},
|
||||||
|
'App.css': {
|
||||||
|
name: 'App.css',
|
||||||
|
language: 'css',
|
||||||
|
value: AppCss
|
||||||
|
},
|
||||||
|
[IMPORT_MAP_FILE_NAME]: {
|
||||||
|
name: IMPORT_MAP_FILE_NAME,
|
||||||
|
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
|
||||||
|
value: importMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reactTemplateFiles = {
|
||||||
|
[ENTRY_FILE_NAME]: {
|
||||||
|
name: ENTRY_FILE_NAME,
|
||||||
|
language: fileNameToLanguage(ENTRY_FILE_NAME),
|
||||||
|
value: main
|
||||||
|
},
|
||||||
|
[IMPORT_MAP_FILE_NAME]: {
|
||||||
|
name: IMPORT_MAP_FILE_NAME,
|
||||||
|
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
|
||||||
|
value: importMap
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { editor } from 'monaco-editor'
|
|||||||
export interface IFile {
|
export interface IFile {
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
language: string
|
language: 'javascript' | 'typescript' | 'json' | 'css'
|
||||||
active?: boolean
|
active?: boolean
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export interface IFiles {
|
|||||||
[key: string]: IFile
|
[key: string]: IFile
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ITheme = 'light' | 'dark'
|
export type ITheme = 'light' | 'vs-dark'
|
||||||
|
|
||||||
export type IImportMap = { imports: Record<string, string> }
|
export type IImportMap = { imports: Record<string, string> }
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export interface ICustomFiles {
|
|||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export type IEditorOptions = editor.IStandaloneEditorConstructionOptions & any
|
export type IEditorOptions = editor.IStandaloneEditorConstructionOptions
|
||||||
export interface IEditorContainer {
|
export interface IEditorContainer {
|
||||||
showFileSelector?: boolean
|
showFileSelector?: boolean
|
||||||
fileSelectorReadOnly?: boolean
|
fileSelectorReadOnly?: boolean
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared.ts'
|
|
||||||
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
|
import { strFromU8, strToU8, unzlibSync, zlibSync } from 'fflate'
|
||||||
import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/files.ts'
|
import { ICustomFiles, IFiles, IImportMap, ITheme } from '@/components/ReactPlayground/shared'
|
||||||
|
import { IMPORT_MAP_FILE_NAME, reactTemplateFiles } from '@/components/ReactPlayground/files'
|
||||||
|
|
||||||
export const strToBase64 = (str: string) => {
|
export const strToBase64 = (str: string) => {
|
||||||
const buffer = strToU8(str)
|
const buffer = strToU8(str)
|
||||||
@@ -22,19 +22,10 @@ export const base64ToStr = (base64: string) => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileNameToLanguage = (name: string) => {
|
|
||||||
const suffix = name.split('.').pop() || ''
|
|
||||||
if (['js', 'jsx'].includes(suffix)) return 'javascript'
|
|
||||||
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
|
|
||||||
if (['json'].includes(suffix)) return 'json'
|
|
||||||
if (['css'].includes(suffix)) return 'css'
|
|
||||||
return 'javascript'
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_DARK_THEME = 'react-playground-prefer-dark'
|
const STORAGE_DARK_THEME = 'react-playground-prefer-dark'
|
||||||
|
|
||||||
export const setPlaygroundTheme = (theme: ITheme) => {
|
export const setPlaygroundTheme = (theme: ITheme) => {
|
||||||
localStorage.setItem(STORAGE_DARK_THEME, String(theme === 'dark'))
|
localStorage.setItem(STORAGE_DARK_THEME, String(theme === 'vs-dark'))
|
||||||
document
|
document
|
||||||
.querySelectorAll('div[data-id="react-playground"]')
|
.querySelectorAll('div[data-id="react-playground"]')
|
||||||
?.forEach((item) => item.setAttribute('class', theme))
|
?.forEach((item) => item.setAttribute('class', theme))
|
||||||
@@ -42,7 +33,7 @@ export const setPlaygroundTheme = (theme: ITheme) => {
|
|||||||
|
|
||||||
export const getPlaygroundTheme = () => {
|
export const getPlaygroundTheme = () => {
|
||||||
const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false')
|
const isDarkTheme = JSON.parse(localStorage.getItem(STORAGE_DARK_THEME) || 'false')
|
||||||
return isDarkTheme ? 'dark' : 'light'
|
return isDarkTheme ? 'vs-dark' : 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformCustomFiles = (files: ICustomFiles) => {
|
const transformCustomFiles = (files: ICustomFiles) => {
|
||||||
@@ -69,6 +60,17 @@ const transformCustomFiles = (files: ICustomFiles) => {
|
|||||||
return newFiles
|
return newFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCustomActiveFile = (files?: ICustomFiles) => {
|
||||||
|
if (!files) return null
|
||||||
|
return Object.keys(files).find((key) => {
|
||||||
|
const tempFile = files[key]
|
||||||
|
if (typeof tempFile !== 'string' && tempFile.active) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMap) => {
|
export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMap) => {
|
||||||
if (!files) return null
|
if (!files) return null
|
||||||
if (importMap) {
|
if (importMap) {
|
||||||
@@ -89,16 +91,6 @@ export const getMergedCustomFiles = (files?: ICustomFiles, importMap?: IImportMa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCustomActiveFile = (files?: ICustomFiles) => {
|
|
||||||
if (!files) return null
|
|
||||||
return Object.keys(files).find((key) => {
|
|
||||||
const tempFile = files[key]
|
|
||||||
if (typeof tempFile !== 'string' && tempFile.active) {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
export const getFilesFromUrl = () => {
|
export const getFilesFromUrl = () => {
|
||||||
let files: IFiles | undefined
|
let files: IFiles | undefined
|
||||||
try {
|
try {
|
||||||
@@ -111,3 +103,12 @@ export const getFilesFromUrl = () => {
|
|||||||
}
|
}
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fileNameToLanguage = (name: string) => {
|
||||||
|
const suffix = name.split('.').pop() || ''
|
||||||
|
if (['js', 'jsx'].includes(suffix)) return 'javascript'
|
||||||
|
if (['ts', 'tsx'].includes(suffix)) return 'typescript'
|
||||||
|
if (['json'].includes(suffix)) return 'json'
|
||||||
|
if (['css'].includes(suffix)) return 'css'
|
||||||
|
return 'javascript'
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import importMap from './template/import-map.json?raw'
|
|
||||||
import AppCss from './template/src/App.css?raw'
|
|
||||||
import App from './template/src/App.tsx?raw'
|
|
||||||
import main from './template/src/main.tsx?raw'
|
|
||||||
import { IFiles } from '@/components/ReactPlayground/shared.ts'
|
|
||||||
import { fileNameToLanguage } from '@/components/ReactPlayground/Utils.ts'
|
|
||||||
|
|
||||||
export const MAIN_FILE_NAME = 'App.tsx'
|
|
||||||
export const IMPORT_MAP_FILE_NAME = 'import-map.json'
|
|
||||||
export const ENTRY_FILE_NAME = 'main.tsx'
|
|
||||||
|
|
||||||
export const initFiles: IFiles = getFilesFromUrl() || {
|
|
||||||
[ENTRY_FILE_NAME]: {
|
|
||||||
name: ENTRY_FILE_NAME,
|
|
||||||
language: fileNameToLanguage(ENTRY_FILE_NAME),
|
|
||||||
value: main
|
|
||||||
},
|
|
||||||
[MAIN_FILE_NAME]: {
|
|
||||||
name: MAIN_FILE_NAME,
|
|
||||||
language: fileNameToLanguage(MAIN_FILE_NAME),
|
|
||||||
value: App
|
|
||||||
},
|
|
||||||
'App.css': {
|
|
||||||
name: 'App.css',
|
|
||||||
language: 'css',
|
|
||||||
value: AppCss
|
|
||||||
},
|
|
||||||
[IMPORT_MAP_FILE_NAME]: {
|
|
||||||
name: IMPORT_MAP_FILE_NAME,
|
|
||||||
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
|
|
||||||
value: importMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reactTemplateFiles = {
|
|
||||||
[ENTRY_FILE_NAME]: {
|
|
||||||
name: ENTRY_FILE_NAME,
|
|
||||||
language: fileNameToLanguage(ENTRY_FILE_NAME),
|
|
||||||
value: main
|
|
||||||
},
|
|
||||||
[IMPORT_MAP_FILE_NAME]: {
|
|
||||||
name: IMPORT_MAP_FILE_NAME,
|
|
||||||
language: fileNameToLanguage(IMPORT_MAP_FILE_NAME),
|
|
||||||
value: importMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Editor from '@/components/ReactPlayground/CodeEditor/Editor'
|
||||||
|
|
||||||
const OnlineEditor: React.FC = () => {
|
const OnlineEditor: React.FC = () => {
|
||||||
return <></>
|
return (
|
||||||
|
<>
|
||||||
|
<Editor
|
||||||
|
theme={'light'}
|
||||||
|
file={{
|
||||||
|
name: 'App.tsx',
|
||||||
|
language: 'typescript',
|
||||||
|
value: 'const a = () => {}'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OnlineEditor
|
export default OnlineEditor
|
||||||
|
|||||||
Reference in New Issue
Block a user