From e3a5543d9f836b468085ea9b7c49db4aa808fb1b Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 1 Aug 2022 22:56:04 +0800 Subject: [PATCH] ui: scratchpad sidebar --- packages/ui-default/build/config/webpack.js | 15 ++- packages/ui-default/bus.ts | 92 +++++++++++++++ .../ui-default/components/monaco/loader.ts | 2 +- .../components/react/DomComponent.tsx | 9 +- .../scratchpad/ScratchpadEditorContainer.jsx | 1 + .../scratchpad/ScratchpadSidebar.tsx | 9 -- .../scratchpad/ScratchpadToolbarContainer.jsx | 11 -- .../components/scratchpad/Tab.page.styl | 24 ++++ .../components/scratchpad/index.jsx | 111 ++++++++++++------ .../components/scratchpad/reducers/index.ts | 2 + .../components/scratchpad/reducers/state.ts | 10 ++ .../components/scratchpad/reducers/ui.ts | 11 +- packages/ui-default/entry.js | 3 + packages/ui-default/package.json | 4 +- 14 files changed, 239 insertions(+), 65 deletions(-) create mode 100644 packages/ui-default/bus.ts delete mode 100644 packages/ui-default/components/scratchpad/ScratchpadSidebar.tsx create mode 100644 packages/ui-default/components/scratchpad/Tab.page.styl create mode 100644 packages/ui-default/components/scratchpad/reducers/state.ts diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js index cdafc825a..9e616f93b 100644 --- a/packages/ui-default/build/config/webpack.js +++ b/packages/ui-default/build/config/webpack.js @@ -106,7 +106,20 @@ export default function (env = {}) { module: { 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()], }, { diff --git a/packages/ui-default/bus.ts b/packages/ui-default/bus.ts new file mode 100644 index 000000000..a16b17e9b --- /dev/null +++ b/packages/ui-default/bus.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-await-in-loop */ + +const _hooks: Record any>> = {}; + +function isBailed(value: any) { + return value !== null && value !== false && value !== undefined; +} + +export type Disposable = () => void; +export type VoidReturn = Promise | any; + +export interface EventMap extends Record { + 'scratchpadEditorCreate': (editor, monaco) => any; +} + +function getHooks(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(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(name: K, listener: EventMap[K]) { + getHooks(name).push(listener); + return () => removeListener(name, listener); +} + +export function prependListener(name: K, listener: EventMap[K]) { + getHooks(name).unshift(listener); + return () => removeListener(name, listener); +} + +export function once(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(name: K, listener: EventMap[K]) { + return addListener(name, listener); +} + +export function off(name: K, listener: EventMap[K]) { + return removeListener(name, listener); +} + +export async function parallel(name: K, ...args: Parameters): Promise { + const tasks: Promise[] = []; + for (const callback of _hooks[name] || []) { + tasks.push(callback.apply(this, args)); + } + await Promise.all(tasks); +} + +export function emit(name: K, ...args: Parameters) { + return parallel(name, ...args); +} + +export async function serial(name: K, ...args: Parameters): Promise { + const hooks = Array.from(_hooks[name] || []); + for (const callback of hooks) { + await callback.apply(this, args); + } +} + +export async function bail(name: K, ...args: Parameters): Promise> { + 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; +} diff --git a/packages/ui-default/components/monaco/loader.ts b/packages/ui-default/components/monaco/loader.ts index 596fc6bfb..754c690ea 100644 --- a/packages/ui-default/components/monaco/loader.ts +++ b/packages/ui-default/components/monaco/loader.ts @@ -39,7 +39,7 @@ export async function load(features = ['markdown']) { for (const feat of features) { if (loaded.includes(feat)) continue; if (!loaders[feat] && !window.externalModules[`monaco-${feat}`]) { - console.error('Unknown monaco feature:', feat); + console.warn('Unknown monaco feature:', feat); continue; } s = Date.now(); diff --git a/packages/ui-default/components/react/DomComponent.tsx b/packages/ui-default/components/react/DomComponent.tsx index 01ae0ee90..d83ca3ec2 100644 --- a/packages/ui-default/components/react/DomComponent.tsx +++ b/packages/ui-default/components/react/DomComponent.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { omit } from 'lodash'; -export default function DomComponent(props: React.HTMLAttributes & { childDom: HTMLElement }) { +const tempDoms = {}; + +export default function DomComponent(props: React.HTMLAttributes & { id?: string, childDom: HTMLElement }) { const ref = React.useRef(); 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 () => { $(ref.current).empty(); }; diff --git a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.jsx b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.jsx index 630970baa..86639a21f 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.jsx +++ b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.jsx @@ -48,6 +48,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e ); window.editor = this.editor; window.monaco = monaco; + window.Hydro.bus.emit('scratchpadEditorCreate', this.editor, monaco); } } diff --git a/packages/ui-default/components/scratchpad/ScratchpadSidebar.tsx b/packages/ui-default/components/scratchpad/ScratchpadSidebar.tsx deleted file mode 100644 index 37f1870ee..000000000 --- a/packages/ui-default/components/scratchpad/ScratchpadSidebar.tsx +++ /dev/null @@ -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 ; - setTimeout(() => setState(state + 1), 1000); - return
No registered providers.
; -} diff --git a/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx b/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx index b6dcd59b1..e9c0abc0d 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx +++ b/packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx @@ -16,7 +16,6 @@ import Toolbar, { const mapStateToProps = (state) => ({ pretestVisible: state.ui.pretest.visible, - sidebarVisible: state.ui.sidebar.visible, recordsVisible: state.ui.records.visible, isPosting: state.ui.isPosting, isRunning: state.pretest.isRunning, @@ -185,16 +184,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool {i18n('Records')} )} - {/* this.props.togglePanel('sidebar')} - data-global-hotkey="alt+w" - data-tooltip={`${i18n('Toggle Sidebar')} (Alt+L)`} - > - - {' '} - {i18n('Pretest')} - */} ); } diff --git a/packages/ui-default/components/scratchpad/Tab.page.styl b/packages/ui-default/components/scratchpad/Tab.page.styl new file mode 100644 index 000000000..fb18cbb47 --- /dev/null +++ b/packages/ui-default/components/scratchpad/Tab.page.styl @@ -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 diff --git a/packages/ui-default/components/scratchpad/index.jsx b/packages/ui-default/components/scratchpad/index.jsx index 410db49e0..6bfbba48f 100644 --- a/packages/ui-default/components/scratchpad/index.jsx +++ b/packages/ui-default/components/scratchpad/index.jsx @@ -1,14 +1,14 @@ import React from 'react'; import SplitPane from 'react-split-pane'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import _ from 'lodash'; 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 ScratchpadToolbar from './ScratchpadToolbarContainer'; import ScratchpadEditor from './ScratchpadEditorContainer'; import ScratchpadPretest from './ScratchpadPretestContainer'; import ScratchpadRecords from './ScratchpadRecordsContainer'; -import ScratchpadSidebar from './ScratchpadSidebar'; function buildNestedPane([a, ...panes], mode = 'horizontal') { const elements = [ @@ -31,12 +31,36 @@ function buildNestedPane([a, ...panes], mode = 'horizontal') { )); } -const mapStateToProps = (state) => ({ - ui: state.ui, -}); +const pages = { + problem: { + icon: () => , + component: () => { + console.log($('.problem-content')); + return ; + }, + }, +}; -const mapDispatchToProps = (dispatch) => ({ - changeUiSize: _.debounce((uiElement, size) => { +let rerenderCallback = null; +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({ type: 'SCRATCHPAD_UI_CHANGE_SIZE', payload: { @@ -44,52 +68,67 @@ const mapDispatchToProps = (dispatch) => ({ size, }, }); - }, 500), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadContainer extends React.PureComponent { - handleChangeSize(uiElement, size) { - this.props.changeUiSize(uiElement, size); $('#scratchpad').trigger('vjScratchpadRelayout'); - this.props.ui.pretest.visible = !!this.props.ui.pretest.visible; - this.forceUpdate(); - } + forceUpdate(); + }, 500); + const switchToPage = (target) => { + dispatch({ + type: 'SCRATCHPAD_SWITCH_TO_PAGE', + payload: target, + }); + }; - render() { - return ( + return ( + +
+ {Object.keys(pages).map((key) => { + const Component = pages[key].icon; + return ( +
switchToPage(key)}> + +
+ ); + })} +
this.handleChangeSize('main', size)} + onChange={(size) => handleChangeSize('main', size)} > - this.handleChangeSize('sidebar', size)} - > - - - +
+ {Object.keys(pages).map((key) => { + const Component = pages[key].component; + return ( +
+ +
+ ); + })} +
{buildNestedPane([ , { - props: this.props.ui.pretest, - onChange: (size) => this.handleChangeSize('pretest', size), + props: ui.pretest, + onChange: (size) => handleChangeSize('pretest', size), element: , }, { - props: this.props.ui.records, - onChange: (size) => this.handleChangeSize('records', size), + props: ui.records, + onChange: (size) => handleChangeSize('records', size), element: , }, ])}
- ); - } -}); +
+ ); +} diff --git a/packages/ui-default/components/scratchpad/reducers/index.ts b/packages/ui-default/components/scratchpad/reducers/index.ts index 833f650b7..363c885cd 100644 --- a/packages/ui-default/components/scratchpad/reducers/index.ts +++ b/packages/ui-default/components/scratchpad/reducers/index.ts @@ -3,12 +3,14 @@ import ui from './ui'; import editor from './editor'; import pretest from './pretest'; import records from './records'; +import state from './state'; const reducer = combineReducers({ ui, editor, pretest, records, + state, }); export default reducer; diff --git a/packages/ui-default/components/scratchpad/reducers/state.ts b/packages/ui-default/components/scratchpad/reducers/state.ts new file mode 100644 index 000000000..5800457be --- /dev/null +++ b/packages/ui-default/components/scratchpad/reducers/state.ts @@ -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; +} diff --git a/packages/ui-default/components/scratchpad/reducers/ui.ts b/packages/ui-default/components/scratchpad/reducers/ui.ts index 3ab5a4203..b3a77d196 100644 --- a/packages/ui-default/components/scratchpad/reducers/ui.ts +++ b/packages/ui-default/components/scratchpad/reducers/ui.ts @@ -11,10 +11,6 @@ export default function reducer(state = { : false, size: 200, }, - sidebar: { - visible: false, - size: 100, - }, records: { visible: UiContext.canViewRecord && localStorage.getItem('scratchpad/records') === 'true', size: 100, @@ -23,6 +19,7 @@ export default function reducer(state = { isPosting: false, waitSec: 0, isWaiting: false, + activePage: 'problem', }, action) { switch (action.type) { 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: return state; } diff --git a/packages/ui-default/entry.js b/packages/ui-default/entry.js index c3cc3f4ea..a3f176dc0 100644 --- a/packages/ui-default/entry.js +++ b/packages/ui-default/entry.js @@ -1,3 +1,5 @@ +import * as bus from './bus'; + window.Hydro = { extraPages: [], preload: [], @@ -5,6 +7,7 @@ window.Hydro = { utils: {}, node_modules: {}, version: process.env.VERSION, + bus, }; window.externalModules = {}; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index ac8e43f1d..a7a3d902c 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.38.29", + "version": "4.38.30", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", @@ -20,6 +20,7 @@ "@fontsource/source-code-pro": "^4.5.10", "@fontsource/ubuntu-mono": "^4.5.10", "@hydrooj/utils": "workspace:*", + "@svgr/webpack": "^5", "@types/gulp-if": "^0.0.34", "@types/jquery": "^3.5.14", "@types/json-schema": "^7.0.11", @@ -111,6 +112,7 @@ "webpackbar": "^5.0.2" }, "dependencies": { + "@vscode/codicons": "^0.0.31", "ansi_up": "^5.1.0", "esbuild": "0.14.3", "fs-extra": "^10.1.0",