diff --git a/resources/scripts/components/elements/AceEditor.tsx b/resources/scripts/components/elements/AceEditor.tsx new file mode 100644 index 000000000..638b6cbba --- /dev/null +++ b/resources/scripts/components/elements/AceEditor.tsx @@ -0,0 +1,142 @@ +import React, { useCallback, useEffect, useState, lazy } from 'react'; +import useRouter from 'use-react-router'; +import { ServerContext } from '@/state/server'; +import ace, { Editor } from 'brace'; +import getFileContents from '@/api/server/files/getFileContents'; +import styled from 'styled-components'; + +// @ts-ignore +require('brace/ext/modelist'); +require('ayu-ace/mirage'); + +const EditorContainer = styled.div` + min-height: 16rem; + height: calc(100vh - 16rem); + ${tw`relative`}; + + #editor { + ${tw`rounded h-full`}; + } +`; + +const modes: { [k: string]: string } = { + // eslint-disable-next-line @typescript-eslint/camelcase + assembly_x86: 'Assembly (x86)', + // eslint-disable-next-line @typescript-eslint/camelcase + c_cpp: 'C++', + coffee: 'Coffeescript', + css: 'CSS', + dockerfile: 'Dockerfile', + golang: 'Go', + html: 'HTML', + ini: 'Ini', + java: 'Java', + javascript: 'Javascript', + json: 'JSON', + kotlin: 'Kotlin', + lua: 'Luascript', + perl: 'Perl', + php: 'PHP', + properties: 'Properties', + python: 'Python', + ruby: 'Ruby', + // eslint-disable-next-line @typescript-eslint/camelcase + plain_text: 'Plaintext', + toml: 'TOML', + typescript: 'Typescript', + xml: 'XML', + yaml: 'YAML', +}; + +Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`)); + +export interface Props { + style?: React.CSSProperties; + fetchContent: (callback: () => Promise) => void; + onContentSaved: (content: string) => void; +} + +export default ({ fetchContent, onContentSaved }: Props) => { + const { location: { hash } } = useRouter(); + const [ content, setContent ] = useState(''); + const [ mode, setMode ] = useState('plain_text'); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + + const [ editor, setEditor ] = useState(); + const ref = useCallback(node => { + if (node) { + setEditor(ace.edit('editor')); + } + }, []); + + useEffect(() => { + getFileContents(uuid, hash.replace(/^#/, '')) + .then(setContent) + .catch(error => console.error(error)); + }, [ uuid, hash ]); + + useEffect(() => { + if (!hash.length) { + return; + } + + const modelist = ace.acequire('ace/ext/modelist'); + if (modelist) { + setMode(modelist.getModeForPath(hash.replace(/^#/, '')).mode); + } + }, [hash]); + + useEffect(() => { + editor && editor.session.setMode(mode); + }, [editor, mode]); + + useEffect(() => { + editor && editor.session.setValue(content); + }, [ editor, content ]); + + useEffect(() => { + if (!editor) { + fetchContent(() => Promise.reject(new Error('no editor session has been configured'))); + return; + } + + editor.setTheme('ace/theme/ayu-mirage'); + + editor.$blockScrolling = Infinity; + editor.container.style.lineHeight = '1.375rem'; + editor.container.style.fontWeight = '500'; + editor.renderer.updateFontSize(); + editor.renderer.setShowPrintMargin(false); + editor.session.setTabSize(4); + editor.session.setUseSoftTabs(true); + + editor.commands.addCommand({ + name: 'Save', + bindKey: { win: 'Ctrl-s', mac: 'Command-s' }, + exec: (editor: Editor) => onContentSaved(editor.session.getValue()), + }); + + fetchContent(() => Promise.resolve(editor.session.getValue())); + }, [ editor, fetchContent, onContentSaved ]); + + return ( + +
+
+
+ +
+
+ + ); +}; diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index 39d39939a..4be49db95 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -1,129 +1,23 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import useRouter from 'use-react-router'; +import React, { lazy } from 'react'; import { ServerContext } from '@/state/server'; -import getFileContents from '@/api/server/files/getFileContents'; -import ace, { Editor } from 'brace'; -import styled from 'styled-components'; -// @ts-ignore -require('brace/ext/modelist'); -require('ayu-ace/mirage'); - -const EditorContainer = styled.div` - min-height: 16rem; - height: calc(100vh - 16rem); - ${tw`relative`}; - - #editor { - ${tw`rounded h-full`}; - } -`; - -const modes: { [k: string]: string } = { - // eslint-disable-next-line @typescript-eslint/camelcase - assembly_x86: 'Assembly (x86)', - // eslint-disable-next-line @typescript-eslint/camelcase - c_cpp: 'C++', - coffee: 'Coffeescript', - css: 'CSS', - dockerfile: 'Dockerfile', - golang: 'Go', - html: 'HTML', - ini: 'Ini', - java: 'Java', - javascript: 'Javascript', - json: 'JSON', - kotlin: 'Kotlin', - lua: 'Luascript', - perl: 'Perl', - php: 'PHP', - properties: 'Properties', - python: 'Python', - ruby: 'Ruby', - // eslint-disable-next-line @typescript-eslint/camelcase - plain_text: 'Plaintext', - toml: 'TOML', - typescript: 'Typescript', - xml: 'XML', - yaml: 'YAML', -}; - -Object.keys(modes).forEach(mode => require(`brace/mode/${mode}`)); +const LazyAceEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/AceEditor')); export default () => { - const { location: { hash } } = useRouter(); - const [ content, setContent ] = useState(''); - const [ mode, setMode ] = useState('plain_text'); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); - const [ editor, setEditor ] = useState(); - const ref = useCallback(node => { - if (node) { - setEditor(ace.edit('editor')); - } - }, []); + let ref: null| (() => Promise) = null; - useEffect(() => { - getFileContents(uuid, hash.replace(/^#/, '')) - .then(setContent) - .catch(error => console.error(error)); - }, [ uuid, hash ]); - - useEffect(() => { - if (!hash.length) { - return; - } - - const modelist = ace.acequire('ace/ext/modelist'); - if (modelist) { - setMode(modelist.getModeForPath(hash.replace(/^#/, '')).mode); - } - }, [hash]); - - useEffect(() => { - editor && editor.session.setMode(mode); - }, [editor, mode]); - - useEffect(() => { - editor && editor.session.setValue(content); - }, [ editor, content ]); - - useEffect(() => { - if (!editor) { - return; - } - - editor.setTheme('ace/theme/ayu-mirage'); - - editor.$blockScrolling = Infinity; - editor.container.style.lineHeight = '1.375rem'; - editor.container.style.fontWeight = '500'; - editor.renderer.updateFontSize(); - editor.renderer.setShowPrintMargin(false); - editor.session.setTabSize(4); - editor.session.setUseSoftTabs(true); - }, [ editor ]); + setTimeout(() => ref && ref().then(console.log), 5000); return (
- -
-
-
- -
-
- + { + ref = value; + }} + onContentSaved={() => null} + />
); }; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 1afd9fcc3..4e5919dd6 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -11,10 +11,7 @@ import DatabasesContainer from '@/components/server/databases/DatabasesContainer import FileManagerContainer from '@/components/server/files/FileManagerContainer'; import { CSSTransition } from 'react-transition-group'; import SuspenseSpinner from '@/components/elements/SuspenseSpinner'; - -const LazyFileEditContainer = lazy>>( - () => import(/* webpackChunkName: "editor" */'@/components/server/files/FileEditContainer') -); +import FileEditContainer from '@/components/server/files/FileEditContainer'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const server = ServerContext.useStoreState(state => state.server.data); @@ -25,7 +22,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) getServer(match.params.id); } - useEffect(() => () => clearServerState(), []); + useEffect(() => () => clearServerState(), [ clearServerState ]); return ( @@ -59,7 +56,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) path={`${match.path}/files/edit`} render={props => ( - + )} exact