ui: scratchpad sidebar

This commit is contained in:
undefined committed 2022-08-01 22:56:04 +08:00
1 parent 981f8a4b9d
commit e3a5543d9f
14 files changed
+239 -65

No files matched your search

+14 -1
View File
@@ -106,7 +106,20 @@ export default function (env = {}) {
module: { module: {
rules: [ rules: [
{ {
test: /\.(svg|ttf|eot|woff|woff2|png|jpg|jpeg|gif)$/, test: /\.svg$/i,
oneOf: [
{
issuer: /\.[jt]sx?$/,
resourceQuery: /react/,
use: { loader: '@svgr/webpack', options: { icon: true } },
},
{
use: [fileLoader()],
},
],
},
{
test: /\.(ttf|eot|woff|woff2|png|jpg|jpeg|gif)$/,
use: [fileLoader()], use: [fileLoader()],
}, },
{ {
+92
View File
@@ -0,0 +1,92 @@
/* eslint-disable no-await-in-loop */
const _hooks: Record<keyof any, Array<(...args: any[]) => any>> = {};
function isBailed(value: any) {
return value !== null && value !== false && value !== undefined;
}
export type Disposable = () => void;
export type VoidReturn = Promise<any> | any;
export interface EventMap extends Record<string, any> {
'scratchpadEditorCreate': (editor, monaco) => any;
}
function getHooks<K extends keyof EventMap>(name: K) {
const hooks = _hooks[name] || (_hooks[name] = []);
if (hooks.length >= 2048) {
console.warn(
'max listener count (2048) for event "%s" exceeded, which may be caused by a memory leak',
name,
);
}
return hooks;
}
export function removeListener<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
const index = (_hooks[name] || []).findIndex((callback) => callback === listener);
if (index >= 0) {
_hooks[name].splice(index, 1);
return true;
}
return false;
}
export function addListener<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
getHooks(name).push(listener);
return () => removeListener(name, listener);
}
export function prependListener<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
getHooks(name).unshift(listener);
return () => removeListener(name, listener);
}
export function once<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
let dispose;
function _listener(...args: any[]) {
dispose();
return listener.apply(this, args);
}
_listener.toString = () => `// Once \n${listener.toString()}`;
dispose = addListener(name, _listener);
return dispose;
}
export function on<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
return addListener(name, listener);
}
export function off<K extends keyof EventMap>(name: K, listener: EventMap[K]) {
return removeListener(name, listener);
}
export async function parallel<K extends keyof EventMap>(name: K, ...args: Parameters<EventMap[K]>): Promise<void> {
const tasks: Promise<any>[] = [];
for (const callback of _hooks[name] || []) {
tasks.push(callback.apply(this, args));
}
await Promise.all(tasks);
}
export function emit<K extends keyof EventMap>(name: K, ...args: Parameters<EventMap[K]>) {
return parallel(name, ...args);
}
export async function serial<K extends keyof EventMap>(name: K, ...args: Parameters<EventMap[K]>): Promise<void> {
const hooks = Array.from(_hooks[name] || []);
for (const callback of hooks) {
await callback.apply(this, args);
}
}
export async function bail<K extends keyof EventMap>(name: K, ...args: Parameters<EventMap[K]>): Promise<ReturnType<EventMap[K]>> {
const hooks = Array.from(_hooks[name] || []);
for (const callback of hooks) {
let result = callback.apply(this, args);
if (result instanceof Promise) result = await result;
if (isBailed(result)) return result;
}
return null;
}
@@ -39,7 +39,7 @@ export async function load(features = ['markdown']) {
for (const feat of features) { for (const feat of features) {
if (loaded.includes(feat)) continue; if (loaded.includes(feat)) continue;
if (!loaders[feat] && !window.externalModules[`monaco-${feat}`]) { if (!loaders[feat] && !window.externalModules[`monaco-${feat}`]) {
console.error('Unknown monaco feature:', feat); console.warn('Unknown monaco feature:', feat);
continue; continue;
} }
s = Date.now(); s = Date.now();
@@ -1,10 +1,15 @@
import React from 'react'; import React from 'react';
import { omit } from 'lodash'; import { omit } from 'lodash';
export default function DomComponent(props: React.HTMLAttributes<HTMLDivElement> & { childDom: HTMLElement }) { const tempDoms = {};
export default function DomComponent(props: React.HTMLAttributes<HTMLDivElement> & { id?: string, childDom: HTMLElement }) {
const ref = React.useRef<HTMLDivElement>(); const ref = React.useRef<HTMLDivElement>();
React.useEffect(() => { React.useEffect(() => {
ref.current.appendChild(props.childDom); let dom = props.childDom;
if (props.id && dom) tempDoms[props.id] = dom;
else if (props.id && !dom && tempDoms[props.id]) dom = tempDoms[props.id];
ref.current.appendChild(dom);
return () => { return () => {
$(ref.current).empty(); $(ref.current).empty();
}; };
@@ -48,6 +48,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e
); );
window.editor = this.editor; window.editor = this.editor;
window.monaco = monaco; window.monaco = monaco;
window.Hydro.bus.emit('scratchpadEditorCreate', this.editor, monaco);
} }
} }
@@ -1,9 +0,0 @@
import React from 'react';
export default function ScratchpadSidebar({ name }) {
const Component = window.Hydro.scratchpad?.[name];
const [state, setState] = React.useState(0);
if (Component) return <Component />;
setTimeout(() => setState(state + 1), 1000);
return <div>No registered providers.</div>;
}
@@ -16,7 +16,6 @@ import Toolbar, {
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
pretestVisible: state.ui.pretest.visible, pretestVisible: state.ui.pretest.visible,
sidebarVisible: state.ui.sidebar.visible,
recordsVisible: state.ui.records.visible, recordsVisible: state.ui.records.visible,
isPosting: state.ui.isPosting, isPosting: state.ui.isPosting,
isRunning: state.pretest.isRunning, isRunning: state.pretest.isRunning,
@@ -185,16 +184,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
{i18n('Records')} {i18n('Records')}
</ToolbarButton> </ToolbarButton>
)} )}
{/* <ToolbarButton
activated={this.props.sidebarVisible}
onClick={() => this.props.togglePanel('sidebar')}
data-global-hotkey="alt+w"
data-tooltip={`${i18n('Toggle Sidebar')} (Alt+L)`}
>
<Icon name="edit" />
{' '}
{i18n('Pretest')}
</ToolbarButton> */}
</Toolbar> </Toolbar>
); );
} }
@@ -0,0 +1,24 @@
.scratchpad__tablist
user-select: none
div
display: flex
width: 50px
height: 50px
font-size: 28px
flex-direction: row
align-items: center
justify-content: center
margin: rem(8px) 0
fill: #666
color: #666
cursor: pointer
&:hover
fill: #000
color: #000
div.scratchpad__tab-active
border-left: 4px solid #000
fill: #000
color: #000
cursor: default
@@ -1,14 +1,14 @@
import React from 'react'; import React from 'react';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import _ from 'lodash'; import _ from 'lodash';
import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent'; import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
import ProblemIcon from '@vscode/codicons/src/icons/file.svg?react';
import Dom from 'vj/components/react/DomComponent'; import Dom from 'vj/components/react/DomComponent';
import ScratchpadToolbar from './ScratchpadToolbarContainer'; import ScratchpadToolbar from './ScratchpadToolbarContainer';
import ScratchpadEditor from './ScratchpadEditorContainer'; import ScratchpadEditor from './ScratchpadEditorContainer';
import ScratchpadPretest from './ScratchpadPretestContainer'; import ScratchpadPretest from './ScratchpadPretestContainer';
import ScratchpadRecords from './ScratchpadRecordsContainer'; import ScratchpadRecords from './ScratchpadRecordsContainer';
import ScratchpadSidebar from './ScratchpadSidebar';
function buildNestedPane([a, ...panes], mode = 'horizontal') { function buildNestedPane([a, ...panes], mode = 'horizontal') {
const elements = [ const elements = [
@@ -31,12 +31,36 @@ function buildNestedPane([a, ...panes], mode = 'horizontal') {
)); ));
} }
const mapStateToProps = (state) => ({ const pages = {
ui: state.ui, problem: {
}); icon: () => <ProblemIcon />,
component: () => {
console.log($('.problem-content'));
return <Dom childDom={$('.problem-content').get(0)} id="problem-content" />;
},
},
};
const mapDispatchToProps = (dispatch) => ({ let rerenderCallback = null;
changeUiSize: _.debounce((uiElement, size) => { export function addPage(key, icon, component) {
pages[key] = {
icon,
component,
};
rerenderCallback?.();
}
window.Hydro.scratchpad = { addPage, pages };
export default function ScratchpadContainer() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
React.useEffect(() => {
rerenderCallback = forceUpdate;
}, []);
const dispatch = useDispatch();
const ui = useSelector((state) => state.ui, _.isEqual);
const handleChangeSize = _.debounce((uiElement, size) => {
dispatch({ dispatch({
type: 'SCRATCHPAD_UI_CHANGE_SIZE', type: 'SCRATCHPAD_UI_CHANGE_SIZE',
payload: { payload: {
@@ -44,52 +68,67 @@ const mapDispatchToProps = (dispatch) => ({
size, size,
}, },
}); });
}, 500),
});
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadContainer extends React.PureComponent {
handleChangeSize(uiElement, size) {
this.props.changeUiSize(uiElement, size);
$('#scratchpad').trigger('vjScratchpadRelayout'); $('#scratchpad').trigger('vjScratchpadRelayout');
this.props.ui.pretest.visible = !!this.props.ui.pretest.visible; forceUpdate();
this.forceUpdate(); }, 500);
} const switchToPage = (target) => {
dispatch({
type: 'SCRATCHPAD_SWITCH_TO_PAGE',
payload: target,
});
};
render() { return (
return ( <SplitPane
split="vertical"
primary="first"
defaultSize={50}
allowResize={false}
>
<div className="scratchpad__tablist">
{Object.keys(pages).map((key) => {
const Component = pages[key].icon;
return (
<div key={key} className={key === ui.activePage ? 'scratchpad__tab-active' : ''} onClick={() => switchToPage(key)}>
<Component />
</div>
);
})}
</div>
<SplitPane <SplitPane
defaultSize={this.props.ui.main.size} defaultSize={ui.main.size}
minSize={250} minSize={250}
split="vertical" split="vertical"
primary="second" primary="second"
onChange={(size) => this.handleChangeSize('main', size)} onChange={(size) => handleChangeSize('main', size)}
> >
<SplitPane <div className="scratchpad__problem">
defaultSize={this.props.ui.sidebar.visible ? this.props.ui.sidebar.size : 0} {Object.keys(pages).map((key) => {
split="vertical" const Component = pages[key].component;
primary="second" return (
onChange={(size) => this.handleChangeSize('sidebar', size)} <div key={key} style={{ display: key === ui.activePage ? 'block' : 'none' }}>
> <Component />
<Dom className="scratchpad__problem" childDom={$('.problem-content').get(0)} /> </div>
<ScratchpadSidebar name="debug" /> );
</SplitPane> })}
</div>
{buildNestedPane([ {buildNestedPane([
<SplitPaneFillOverlay key="editor" className="flex-col"> <SplitPaneFillOverlay key="editor" className="flex-col">
<ScratchpadToolbar /> <ScratchpadToolbar />
<ScratchpadEditor /> <ScratchpadEditor />
</SplitPaneFillOverlay>, </SplitPaneFillOverlay>,
{ {
props: this.props.ui.pretest, props: ui.pretest,
onChange: (size) => this.handleChangeSize('pretest', size), onChange: (size) => handleChangeSize('pretest', size),
element: <ScratchpadPretest key="pretest" />, element: <ScratchpadPretest key="pretest" />,
}, },
{ {
props: this.props.ui.records, props: ui.records,
onChange: (size) => this.handleChangeSize('records', size), onChange: (size) => handleChangeSize('records', size),
element: <ScratchpadRecords key="records" />, element: <ScratchpadRecords key="records" />,
}, },
])} ])}
</SplitPane> </SplitPane>
); </SplitPane>
} );
}); }
@@ -3,12 +3,14 @@ import ui from './ui';
import editor from './editor'; import editor from './editor';
import pretest from './pretest'; import pretest from './pretest';
import records from './records'; import records from './records';
import state from './state';
const reducer = combineReducers({ const reducer = combineReducers({
ui, ui,
editor, editor,
pretest, pretest,
records, records,
state,
}); });
export default reducer; export default reducer;
@@ -0,0 +1,10 @@
export default function reducer(state = {}, action) {
if (action.type === 'SCRATCHPAD_STATE_UPDATE') {
const { key, value } = action.payload;
return {
...state,
[key]: value,
};
}
return state;
}
@@ -11,10 +11,6 @@ export default function reducer(state = {
: false, : false,
size: 200, size: 200,
}, },
sidebar: {
visible: false,
size: 100,
},
records: { records: {
visible: UiContext.canViewRecord && localStorage.getItem('scratchpad/records') === 'true', visible: UiContext.canViewRecord && localStorage.getItem('scratchpad/records') === 'true',
size: 100, size: 100,
@@ -23,6 +19,7 @@ export default function reducer(state = {
isPosting: false, isPosting: false,
waitSec: 0, waitSec: 0,
isWaiting: false, isWaiting: false,
activePage: 'problem',
}, action) { }, action) {
switch (action.type) { switch (action.type) {
case 'SCRATCHPAD_UI_CHANGE_SIZE': { case 'SCRATCHPAD_UI_CHANGE_SIZE': {
@@ -122,6 +119,12 @@ export default function reducer(state = {
}, },
}; };
} }
case 'SCRATCHPAD_SWITCH_TO_PAGE': {
return {
...state,
activePage: action.payload,
};
}
default: default:
return state; return state;
} }
+3
View File
@@ -1,3 +1,5 @@
import * as bus from './bus';
window.Hydro = { window.Hydro = {
extraPages: [], extraPages: [],
preload: [], preload: [],
@@ -5,6 +7,7 @@ window.Hydro = {
utils: {}, utils: {},
node_modules: {}, node_modules: {},
version: process.env.VERSION, version: process.env.VERSION,
bus,
}; };
window.externalModules = {}; window.externalModules = {};
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.38.29", "version": "4.38.30",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -20,6 +20,7 @@
"@fontsource/source-code-pro": "^4.5.10", "@fontsource/source-code-pro": "^4.5.10",
"@fontsource/ubuntu-mono": "^4.5.10", "@fontsource/ubuntu-mono": "^4.5.10",
"@hydrooj/utils": "workspace:*", "@hydrooj/utils": "workspace:*",
"@svgr/webpack": "^5",
"@types/gulp-if": "^0.0.34", "@types/gulp-if": "^0.0.34",
"@types/jquery": "^3.5.14", "@types/jquery": "^3.5.14",
"@types/json-schema": "^7.0.11", "@types/json-schema": "^7.0.11",
@@ -111,6 +112,7 @@
"webpackbar": "^5.0.2" "webpackbar": "^5.0.2"
}, },
"dependencies": { "dependencies": {
"@vscode/codicons": "^0.0.31",
"ansi_up": "^5.1.0", "ansi_up": "^5.1.0",
"esbuild": "0.14.3", "esbuild": "0.14.3",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",