diff --git a/packages/ui-default/.eslintrc.js b/packages/ui-default/.eslintrc.js index 66d10a9e..a99a7c33 100644 --- a/packages/ui-default/.eslintrc.js +++ b/packages/ui-default/.eslintrc.js @@ -3,8 +3,8 @@ const path = require('path'); module.exports = { root: true, - parser: '@babel/eslint-parser', - plugins: ['babel', 'react'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'react'], env: { browser: true, es6: true, @@ -14,15 +14,14 @@ module.exports = { extends: ['airbnb'], parserOptions: { sourceType: 'module', - ecmaVersion: 7, + ecmaVersion: 2020, ecmaFeatures: { impliedStrict: true, experimentalObjectRestSpread: true, jsx: true, + defaultParams: true, legacyDecorators: true, - }, - babelOptions: { - configFile: `${__dirname}/babel.config.js`, + allowImportExportEverywhere: true, }, }, settings: { @@ -48,21 +47,25 @@ module.exports = { window: true, }, rules: { + '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/no-implied-eval': 'off', + '@typescript-eslint/no-throw-literal': 'off', + '@typescript-eslint/return-await': 'off', + // FIXME A bug with eslint-parser 'template-curly-spacing': 'off', - indent: 'off', 'comma-dangle': [ 'error', 'always-multiline', ], - // indent: [ - // 'error', - // 2, - // { - // SwitchCase: 0, - // }, - // ], + indent: [ + 'error', + 2, + { + SwitchCase: 0, + }, + ], 'max-len': ['error', 150], quotes: 'warn', 'class-methods-use-this': 'off', diff --git a/packages/ui-default/babel.config.js b/packages/ui-default/babel.config.js deleted file mode 100644 index 932e4360..00000000 --- a/packages/ui-default/babel.config.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - plugins: [ - 'lodash', - '@babel/plugin-transform-runtime', - '@babel/plugin-syntax-dynamic-import', - '@babel/plugin-syntax-import-meta', - '@babel/plugin-proposal-json-strings', - ['@babel/plugin-proposal-class-properties', { loose: true }], - '@babel/plugin-proposal-function-sent', - '@babel/plugin-proposal-export-namespace-from', - '@babel/plugin-proposal-numeric-separator', - '@babel/plugin-proposal-throw-expressions', - [ - 'prismjs', - { - languages: [ - 'clike', - 'c', - 'cpp', - 'pascal', - 'java', - 'python', - 'java', - 'python', - 'php', - 'rust', - 'haskell', - 'javascript', - 'go', - 'ruby', - 'csharp', - 'julia', - ], - plugins: [ - 'toolbar', - 'line-numbers', - ], - css: true, - }, - ], - ], - presets: [ - ['@babel/preset-env', { loose: true, modules: false }], - '@babel/preset-react', - ], -}; diff --git a/packages/ui-default/build/index.js b/packages/ui-default/build/index.js index 8437aafa..9f76ce82 100644 --- a/packages/ui-default/build/index.js +++ b/packages/ui-default/build/index.js @@ -1,7 +1,34 @@ -require('@babel/register')({ - plugins: ['@babel/plugin-transform-runtime'], - presets: [['@babel/preset-env', { loose: true }]], -}); +const esbuild = require('esbuild'); +const fs = require('fs'); + +let transformTimeUsage = 0; +let transformCount = 0; +let displayTimeout; +function transform(filename) { + const start = new Date(); + const result = esbuild.buildSync({ + entryPoints: [filename], + sourcemap: 'inline', + platform: 'node', + format: 'cjs', + target: 'node12', + jsx: 'transform', + write: false, + }); + if (result.warnings.length) console.warn(result.warnings); + transformTimeUsage += new Date().getTime() - start.getTime(); + transformCount++; + if (displayTimeout) clearTimeout(displayTimeout); + displayTimeout = setTimeout(() => console.log(`Transformed ${transformCount} files. (${transformTimeUsage}ms)`), 1000); + return result.outputFiles[0].text; +} +require.extensions['.js'] = function loader(module, filename) { + if (!filename.includes('node_modules')) { + return module._compile(transform(filename), filename); + } + const content = fs.readFileSync(filename, 'utf-8'); + return module._compile(content, filename); +}; const main = require('./main.js'); if (!module.parent) main(); diff --git a/packages/ui-default/components/calendar/index.js b/packages/ui-default/components/calendar/index.js index 00f001da..2893901c 100644 --- a/packages/ui-default/components/calendar/index.js +++ b/packages/ui-default/components/calendar/index.js @@ -279,7 +279,7 @@ export default class Calendar { for (; vIndex < vIndexMax; ++vIndex) { if (_.every(_ .range(beginDay, endDay + 1) - .map((day) => !dayBitmap[day][vIndex]) // eslint-disable-line no-loop-func + .map((day) => !dayBitmap[day][vIndex]), // eslint-disable-line no-loop-func )) { // eslint-disable-line function-paren-newline break; } diff --git a/packages/ui-default/components/contest/contest.page.js b/packages/ui-default/components/contest/contest.page.js index ed8c1398..420dc95f 100644 --- a/packages/ui-default/components/contest/contest.page.js +++ b/packages/ui-default/components/contest/contest.page.js @@ -5,20 +5,20 @@ import request from 'vj/utils/request'; import Notification from 'vj/components/notification'; const contestPage = new AutoloadPage('contestPage', () => { - $('[data-contest-code]').on('click', (ev) => { - ev.preventDefault(); - // eslint-disable-next-line no-alert - const code = prompt(i18n('Invitation code:')); - request.post('', { - operation: 'attend', - code, - }).then(() => { - Notification.success(i18n('Successfully attended')); - delay(1000).then(() => window.location.reload()); - }).catch((e) => { - Notification.error(e.message || e); - }); + $('[data-contest-code]').on('click', (ev) => { + ev.preventDefault(); + // eslint-disable-next-line no-alert + const code = prompt(i18n('Invitation code:')); + request.post('', { + operation: 'attend', + code, + }).then(() => { + Notification.success(i18n('Successfully attended')); + delay(1000).then(() => window.location.reload()); + }).catch((e) => { + Notification.error(e.message || e); }); + }); }); export default contestPage; diff --git a/packages/ui-default/components/dialog/DomDialog.js b/packages/ui-default/components/dialog/DomDialog.js index 0487516e..df5f5675 100644 --- a/packages/ui-default/components/dialog/DomDialog.js +++ b/packages/ui-default/components/dialog/DomDialog.js @@ -42,7 +42,7 @@ export default class DomDialog extends DOMAttachedObject { { duration: 100, easing: 'easeOutCubic', - } + }, ); const $dgContent = this.$dom.find('.dialog__content'); diff --git a/packages/ui-default/components/media/media.page.js b/packages/ui-default/components/media/media.page.js index 6ccf07cb..5684ec6e 100644 --- a/packages/ui-default/components/media/media.page.js +++ b/packages/ui-default/components/media/media.page.js @@ -2,42 +2,42 @@ import { AutoloadPage } from 'vj/misc/Page'; import request from 'vj/utils/request'; export default new AutoloadPage('media', async () => { - async function parseMedia($dom = $(document.body)) { - const items = []; - const resolvers = []; - const users = $dom.find('div[data-user]'); - const resolve = (ele, item) => { - items.push(item); - resolvers.push((html) => html && $(ele).replaceWith($(html))); - }; - users.get().forEach((ele) => resolve(ele, { type: 'user', id: +$(ele).text() })); - $dom.find('.typo').get().forEach((el) => { - $(el).find('a[href]').get().forEach((ele) => { - if ($(ele).parent().hasClass('user-profile-link')) return; - let target = $(ele).attr('href'); - let { domainId } = UiContext; - if (target.startsWith(UiContext.url_prefix)) { - target.replace(UiContext.url_prefix, ''); - if (!target.startsWith('/')) target = `/${target}`; - } - if (!target.startsWith('/') || target.startsWith('//')) return; - if (target.startsWith('/d/')) { - const [, , domain, ...extra] = target.split('/'); - domainId = domain; - target = `/${extra.join('/')}`; - } - const [, category, data, extra] = target.split('/'); - if (!data) return; - if (category === 'user' && Number.isInteger(+data) && !extra) resolve(ele, { type: 'user', id: +data }); - if (category === 'p' && !extra) resolve(ele, { type: 'problem', id: data, domainId }); - if (category === 'contest' && !extra) resolve(ele, { type: 'contest', id: data, domainId }); - }); - }); - if (!items.length) return; - const res = await request.post(`/d/${UiContext.domainId}/media`, { items }); - for (let i = 0; i < res.length; i++) resolvers[i](res[i]); - } + async function parseMedia($dom = $(document.body)) { + const items = []; + const resolvers = []; + const users = $dom.find('div[data-user]'); + const resolve = (ele, item) => { + items.push(item); + resolvers.push((html) => html && $(ele).replaceWith($(html))); + }; + users.get().forEach((ele) => resolve(ele, { type: 'user', id: +$(ele).text() })); + $dom.find('.typo').get().forEach((el) => { + $(el).find('a[href]').get().forEach((ele) => { + if ($(ele).parent().hasClass('user-profile-link')) return; + let target = $(ele).attr('href'); + let { domainId } = UiContext; + if (target.startsWith(UiContext.url_prefix)) { + target.replace(UiContext.url_prefix, ''); + if (!target.startsWith('/')) target = `/${target}`; + } + if (!target.startsWith('/') || target.startsWith('//')) return; + if (target.startsWith('/d/')) { + const [, , domain, ...extra] = target.split('/'); + domainId = domain; + target = `/${extra.join('/')}`; + } + const [, category, data, extra] = target.split('/'); + if (!data) return; + if (category === 'user' && Number.isInteger(+data) && !extra) resolve(ele, { type: 'user', id: +data }); + if (category === 'p' && !extra) resolve(ele, { type: 'problem', id: data, domainId }); + if (category === 'contest' && !extra) resolve(ele, { type: 'contest', id: data, domainId }); + }); + }); + if (!items.length) return; + const res = await request.post(`/d/${UiContext.domainId}/media`, { items }); + for (let i = 0; i < res.length; i++) resolvers[i](res[i]); + } - await parseMedia(); - $(document).on('vjContentNew', (e) => parseMedia($(e.target))); + await parseMedia(); + $(document).on('vjContentNew', (e) => parseMedia($(e.target))); }); diff --git a/packages/ui-default/components/notification/notification.page.js b/packages/ui-default/components/notification/notification.page.js index 516a503d..800c54ce 100644 --- a/packages/ui-default/components/notification/notification.page.js +++ b/packages/ui-default/components/notification/notification.page.js @@ -3,12 +3,12 @@ import Notification from 'vj/components/notification/index'; import i18n from 'vj/utils/i18n'; export default new AutoloadPage('notificationPage', (pagename) => { - const message = i18n(`Hint::Page::${pagename}`); - const item = localStorage.getItem(`hint.${message}`); - if (message !== `Hint::Page::${pagename}` && !item) { - Notification.info(message, message.length * 500); - localStorage.setItem(`hint.${message}`, true); - } - const text = new URL(window.location.href).searchParams.get('notification'); - if (text) Notification.success(text); + const message = i18n(`Hint::Page::${pagename}`); + const item = localStorage.getItem(`hint.${message}`); + if (message !== `Hint::Page::${pagename}` && !item) { + Notification.info(message, message.length * 500); + localStorage.setItem(`hint.${message}`, true); + } + const text = new URL(window.location.href).searchParams.get('notification'); + if (text) Notification.success(text); }); diff --git a/packages/ui-default/components/problem/list.page.js b/packages/ui-default/components/problem/list.page.js index f9515ec3..13ad2d58 100644 --- a/packages/ui-default/components/problem/list.page.js +++ b/packages/ui-default/components/problem/list.page.js @@ -1,8 +1,8 @@ import { AutoloadPage } from 'vj/misc/Page'; export default new AutoloadPage('problemListPage', () => { + $('.col--problem-name>a').attr('target', '_blank'); + $(document).on('vjContentNew', () => { $('.col--problem-name>a').attr('target', '_blank'); - $(document).on('vjContentNew', () => { - $('.col--problem-name>a').attr('target', '_blank'); - }); + }); }); diff --git a/packages/ui-default/components/scratchpad/reducers/editor.js b/packages/ui-default/components/scratchpad/reducers/editor.js index 654cba17..c11fbaf9 100644 --- a/packages/ui-default/components/scratchpad/reducers/editor.js +++ b/packages/ui-default/components/scratchpad/reducers/editor.js @@ -3,20 +3,20 @@ export default function reducer(state = { code: localStorage.getItem(`${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`) || UiContext.codeTemplate, }, action) { switch (action.type) { - case 'SCRATCHPAD_EDITOR_UPDATE_CODE': { - localStorage.setItem(`${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`, action.payload); - return { - ...state, - code: action.payload, - }; - } - case 'SCRATCHPAD_EDITOR_SET_LANG': { - return { - ...state, - lang: action.payload, - }; - } - default: - return state; + case 'SCRATCHPAD_EDITOR_UPDATE_CODE': { + localStorage.setItem(`${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`, action.payload); + return { + ...state, + code: action.payload, + }; + } + case 'SCRATCHPAD_EDITOR_SET_LANG': { + return { + ...state, + lang: action.payload, + }; + } + default: + return state; } } diff --git a/packages/ui-default/components/scratchpad/reducers/ui.js b/packages/ui-default/components/scratchpad/reducers/ui.js index bfe6c1c0..7cad461d 100644 --- a/packages/ui-default/components/scratchpad/reducers/ui.js +++ b/packages/ui-default/components/scratchpad/reducers/ui.js @@ -17,88 +17,88 @@ export default function reducer(state = { isPosting: false, }, action) { switch (action.type) { - case 'SCRATCHPAD_UI_CHANGE_SIZE': { - const { uiElement, size } = action.payload; - return { - ...state, - [uiElement]: { - ...state[uiElement], - size, - }, - }; - } - case 'SCRATCHPAD_UI_SET_VISIBILITY': { - const { uiElement, visibility } = action.payload; - return { - ...state, - [uiElement]: { - ...state[uiElement], - visible: visibility, - }, - }; - } - case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': { - const { uiElement } = action.payload; - return { - ...state, - [uiElement]: { - ...state[uiElement], - visible: !state[uiElement].visible, - }, - }; - } - case 'SCRATCHPAD_POST_PRETEST_PENDING': - case 'SCRATCHPAD_POST_SUBMIT_PENDING': { - return { - ...state, - isPosting: true, - }; - } - case 'SCRATCHPAD_POST_PRETEST_FULFILLED': - case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': { - Notification.success(i18n('Submitted.')); - return { - ...state, - isPosting: false, - }; - } - case 'SCRATCHPAD_POST_PRETEST_REJECTED': - case 'SCRATCHPAD_POST_SUBMIT_REJECTED': { - Notification.error(action.payload.message); - return { - ...state, - isPosting: false, - }; - } - case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': { - return { - ...state, - records: { - ...state.records, - isLoading: true, - }, - }; - } - case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': { - Notification.error(action.payload.message); - return { - ...state, - records: { - ...state.records, - isLoading: false, - }, - }; - } - case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': { - return { - ...state, - records: { - ...state.records, - isLoading: false, - }, - }; - } - default: - return state; + case 'SCRATCHPAD_UI_CHANGE_SIZE': { + const { uiElement, size } = action.payload; + return { + ...state, + [uiElement]: { + ...state[uiElement], + size, + }, + }; + } + case 'SCRATCHPAD_UI_SET_VISIBILITY': { + const { uiElement, visibility } = action.payload; + return { + ...state, + [uiElement]: { + ...state[uiElement], + visible: visibility, + }, + }; + } + case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': { + const { uiElement } = action.payload; + return { + ...state, + [uiElement]: { + ...state[uiElement], + visible: !state[uiElement].visible, + }, + }; + } + case 'SCRATCHPAD_POST_PRETEST_PENDING': + case 'SCRATCHPAD_POST_SUBMIT_PENDING': { + return { + ...state, + isPosting: true, + }; + } + case 'SCRATCHPAD_POST_PRETEST_FULFILLED': + case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': { + Notification.success(i18n('Submitted.')); + return { + ...state, + isPosting: false, + }; + } + case 'SCRATCHPAD_POST_PRETEST_REJECTED': + case 'SCRATCHPAD_POST_SUBMIT_REJECTED': { + Notification.error(action.payload.message); + return { + ...state, + isPosting: false, + }; + } + case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': { + return { + ...state, + records: { + ...state.records, + isLoading: true, + }, + }; + } + case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': { + Notification.error(action.payload.message); + return { + ...state, + records: { + ...state.records, + isLoading: false, + }, + }; + } + case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': { + return { + ...state, + records: { + ...state.records, + isLoading: false, + }, + }; + } + default: + return state; } } diff --git a/packages/ui-default/components/tab/Tab.js b/packages/ui-default/components/tab/Tab.js index 97948211..611fb889 100644 --- a/packages/ui-default/components/tab/Tab.js +++ b/packages/ui-default/components/tab/Tab.js @@ -66,7 +66,7 @@ export default class Tab extends DOMAttachedObject { { duration: TAB_TRANSITION_DURATION, easing: 'linear', - } + }, ); $newTab .transition( @@ -74,7 +74,7 @@ export default class Tab extends DOMAttachedObject { { duration: TAB_TRANSITION_DURATION, easing: 'linear', - } + }, ); await this.$content .transition({ diff --git a/packages/ui-default/components/wastyle/index.js b/packages/ui-default/components/wastyle/index.js index 3e378c76..e2af38d6 100644 --- a/packages/ui-default/components/wastyle/index.js +++ b/packages/ui-default/components/wastyle/index.js @@ -1,16 +1,16 @@ import astyleBinaryUrl from 'wastyle/dist/astyle-optimize-size.wasm'; export default async function load() { - const { init, format } = await import('wastyle'); - try { - await init(astyleBinaryUrl); - console.log('WAstyle is ready!'); - const formatter = (code, options) => { - const [success, result] = format(code, options); - return [success, (result || '').replace(/^#(include|import)[\t ]*(<|")/gm, (match, p1, p2) => `#${p1} ${p2}`)]; - }; - return [true, formatter]; - } catch (e) { - return [false, e.message]; - } + const { init, format } = await import('wastyle'); + try { + await init(astyleBinaryUrl); + console.log('WAstyle is ready!'); + const formatter = (code, options) => { + const [success, result] = format(code, options); + return [success, (result || '').replace(/^#(include|import)[\t ]*(<|")/gm, (match, p1, p2) => `#${p1} ${p2}`)]; + }; + return [true, formatter]; + } catch (e) { + return [false, e.message]; + } } diff --git a/packages/ui-default/components/zipDownloader/index.js b/packages/ui-default/components/zipDownloader/index.js index 59ccab58..c1c524b3 100644 --- a/packages/ui-default/components/zipDownloader/index.js +++ b/packages/ui-default/components/zipDownloader/index.js @@ -86,7 +86,7 @@ export async function downloadProblemSet(pids, name = 'Export') { }); let { links } = await request.post( `/d/${UiContext.domainId}/p/${pid}/files`, - { operation: 'get_links', files: (pdoc.data || []).map((i) => i.name), type: 'testdata' } + { operation: 'get_links', files: (pdoc.data || []).map((i) => i.name), type: 'testdata' }, ); for (const filename of Object.keys(links)) { targets.push({ filename: `${pid}/testdata/${filename}`, url: links[filename] }); diff --git a/packages/ui-default/entry.js b/packages/ui-default/entry.js index 3caa6cdd..287a9f72 100644 --- a/packages/ui-default/entry.js +++ b/packages/ui-default/entry.js @@ -16,6 +16,6 @@ console.log( / __ / /_/ / /_/ / / / /_/ / /_/ /_/\\__, /\\__,_/_/ \\____/ /____/ -` +`, ); import('./hydro'); diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 82f9696f..008b0de0 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,28 +1,15 @@ { "name": "@hydrooj/ui-default", - "version": "4.16.4", + "version": "4.17.0", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", "repository": "https://github.com/hydro-dev/Hydro.git", "preferUnplugged": true, + "scripts": { + "lint": "eslint ." + }, "devDependencies": { - "@babel/cli": "^7.14.8", - "@babel/core": "^7.15.0", - "@babel/eslint-parser": "7.15.0", - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-export-namespace-from": "^7.14.5", - "@babel/plugin-proposal-function-sent": "^7.14.5", - "@babel/plugin-proposal-json-strings": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-throw-expressions": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-transform-runtime": "^7.15.0", - "@babel/preset-env": "^7.15.0", - "@babel/preset-react": "^7.14.5", - "@babel/register": "^7.14.5", - "@babel/runtime-corejs3": "^7.14.9", "@blockly/block-extension-tooltip": "^1.0.15", "@blueprintjs/core": "^3.47.0", "@blueprintjs/icons": "^3.27.0", @@ -33,22 +20,20 @@ "@undefined-moe/monaco-yaml": "^2.5.0", "ansi_up": "^5.0.1", "autoprefixer": "^9.8.6", - "babel-loader": "^8.2.2", - "babel-plugin-lodash": "^3.3.4", - "babel-plugin-prismjs": "^2.1.0", "blockly": "^6.20210701.0", "chalk": "^4.1.2", "classnames": "^2.3.1", + "clean-webpack-plugin": "^4.0.0-alpha.0", "clipboard": "^2.0.8", "copy-webpack-plugin": "^6.4.1", "css-loader": "^4.3.0", "diff-dom": "^4.2.2", "echarts": "^5.1.2", "emojify.js": "^1.1.0", + "esbuild-loader": "^2.14.0", "eslint": "^7.32.0", "eslint-config-airbnb": "^18.2.1", "eslint-import-resolver-webpack": "^0.13.1", - "eslint-plugin-babel": "^5.3.1", "eslint-plugin-import": "^2.23.4", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^7.24.0", @@ -111,9 +96,7 @@ "webpackbar": "^5.0.0-3" }, "dependencies": { - "clean-webpack-plugin": "^4.0.0-alpha.0", "esbuild": "^0.12.18", - "esbuild-loader": "^2.14.0", "js-yaml": "^4.1.0", "jsesc": "^3.0.2", "katex": "^0.13.13", diff --git a/packages/ui-default/pages/home_files.page.js b/packages/ui-default/pages/home_files.page.js index c52553b5..03e59b8b 100644 --- a/packages/ui-default/pages/home_files.page.js +++ b/packages/ui-default/pages/home_files.page.js @@ -9,37 +9,37 @@ import tpl from 'vj/utils/tpl'; import i18n from 'vj/utils/i18n'; function onBeforeUnload(e) { - e.returnValue = ''; + e.returnValue = ''; } const page = new NamedPage('home_files', () => { - function ensureAndGetSelectedFiles() { - const files = _.map( - $('.home-files tbody [data-checkbox-group="user_files"]:checked'), - (ch) => $(ch).closest('tr').attr('data-filename'), - ); - if (files.length === 0) { - Notification.error(i18n('Please select at least one file to perform this operation.')); - return null; - } - return files; + function ensureAndGetSelectedFiles() { + const files = _.map( + $('.home-files tbody [data-checkbox-group="user_files"]:checked'), + (ch) => $(ch).closest('tr').attr('data-filename'), + ); + if (files.length === 0) { + Notification.error(i18n('Please select at least one file to perform this operation.')); + return null; } + return files; + } - async function handleClickUpload(files) { - if (!files) { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = true; - input.click(); - await new Promise((resolve) => { input.onchange = resolve; }); - files = input.files; - } - if (!files.length) { - Notification.warn(i18n('No file selected.')); - return; - } - const dialog = new Dialog({ - $body: ` + async function handleClickUpload(files) { + if (!files) { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.click(); + await new Promise((resolve) => { input.onchange = resolve; }); + files = input.files; + } + if (!files.length) { + Notification.warn(i18n('No file selected.')); + return; + } + const dialog = new Dialog({ + $body: `
@@ -48,127 +48,127 @@ const page = new NamedPage('home_files', () => {
`, + }); + try { + Notification.info(i18n('Uploading files...')); + window.addEventListener('beforeunload', onBeforeUnload); + dialog.open(); + const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label'); + const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress'); + const $fileLabel = dialog.$dom.find('.dialog__body .file-label'); + const $fileProgress = dialog.$dom.find('.dialog__body .file-progress'); + for (const i in files) { + if (Number.isNaN(+i)) continue; + const file = files[i]; + const data = new FormData(); + data.append('filename', file.name); + data.append('file', file); + data.append('operation', 'upload_file'); + await request.postFile('', data, { + xhr() { + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('loadstart', () => { + $fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`); + $fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`); + $uploadLabel.text(i18n('Uploading... ({0}%)', 0)); + $uploadProgress.width(0); + }); + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + const percentComplete = Math.round((e.loaded / e.total) * 100); + if (percentComplete === 100) $uploadLabel.text(i18n('Processing...')); + else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete)); + $uploadProgress.width(`${percentComplete}%`); + } + }, false); + return xhr; + }, }); - try { - Notification.info(i18n('Uploading files...')); - window.addEventListener('beforeunload', onBeforeUnload); - dialog.open(); - const $uploadLabel = dialog.$dom.find('.dialog__body .upload-label'); - const $uploadProgress = dialog.$dom.find('.dialog__body .upload-progress'); - const $fileLabel = dialog.$dom.find('.dialog__body .file-label'); - const $fileProgress = dialog.$dom.find('.dialog__body .file-progress'); - for (const i in files) { - if (Number.isNaN(+i)) continue; - const file = files[i]; - const data = new FormData(); - data.append('filename', file.name); - data.append('file', file); - data.append('operation', 'upload_file'); - await request.postFile('', data, { - xhr() { - const xhr = new XMLHttpRequest(); - xhr.upload.addEventListener('loadstart', () => { - $fileLabel.text(`[${+i + 1}/${files.length}] ${file.name}`); - $fileProgress.width(`${Math.round((+i + 1) / files.length * 100)}%`); - $uploadLabel.text(i18n('Uploading... ({0}%)', 0)); - $uploadProgress.width(0); - }); - xhr.upload.addEventListener('progress', (e) => { - if (e.lengthComputable) { - const percentComplete = Math.round((e.loaded / e.total) * 100); - if (percentComplete === 100) $uploadLabel.text(i18n('Processing...')); - else $uploadLabel.text(i18n('Uploading... ({0}%)', percentComplete)); - $uploadProgress.width(`${percentComplete}%`); - } - }, false); - return xhr; - }, - }); - } - window.removeEventListener('beforeunload', onBeforeUnload); - Notification.success(i18n('File uploaded successfully.')); - await pjax.request({ push: false }); - } catch (e) { - console.error(e); - Notification.error(i18n('File upload failed: {0}', e.toString())); - } finally { - dialog.close(); - } + } + window.removeEventListener('beforeunload', onBeforeUnload); + Notification.success(i18n('File uploaded successfully.')); + await pjax.request({ push: false }); + } catch (e) { + console.error(e); + Notification.error(i18n('File upload failed: {0}', e.toString())); + } finally { + dialog.close(); } + } - async function handleClickRemoveSelected() { - const selectedFiles = ensureAndGetSelectedFiles(); - if (selectedFiles === null) return; - const action = await new ConfirmDialog({ - $body: tpl` + async function handleClickRemoveSelected() { + const selectedFiles = ensureAndGetSelectedFiles(); + if (selectedFiles === null) return; + const action = await new ConfirmDialog({ + $body: tpl`

${i18n('Confirm to delete the selected files?')}

`, - }).open(); - if (action !== 'yes') return; - try { - await request.post('', { - operation: 'delete_files', - files: selectedFiles, - }); - Notification.success(i18n('Selected files have been deleted.')); - await pjax.request({ push: false }); - } catch (error) { - Notification.error(error.message); - } + }).open(); + if (action !== 'yes') return; + try { + await request.post('', { + operation: 'delete_files', + files: selectedFiles, + }); + Notification.success(i18n('Selected files have been deleted.')); + await pjax.request({ push: false }); + } catch (error) { + Notification.error(error.message); } + } - /** + /** * @param {JQuery.DragOverEvent} ev */ - function handleDragOver(ev) { - ev.preventDefault(); - // TODO display a drag-drop allowed hint - } + function handleDragOver(ev) { + ev.preventDefault(); + // TODO display a drag-drop allowed hint + } - /** + /** * @param {JQuery.DropEvent} ev */ - function handleDrop(ev) { - ev.preventDefault(); - if (!$('[name="upload_testdata"]').length) { - Notification.error(i18n("You don't have permission to upload file.")); - return; - } - ev = ev.originalEvent; - const files = []; - if (ev.dataTransfer.items) { - for (let i = 0; i < ev.dataTransfer.items.length; i++) { - if (ev.dataTransfer.items[i].kind === 'file') { - const file = ev.dataTransfer.items[i].getAsFile(); - files.push(file); - } - } - } else { - for (let i = 0; i < ev.dataTransfer.files.length; i++) { - files.push(ev.dataTransfer.files[i]); - } + function handleDrop(ev) { + ev.preventDefault(); + if (!$('[name="upload_testdata"]').length) { + Notification.error(i18n("You don't have permission to upload file.")); + return; + } + ev = ev.originalEvent; + const files = []; + if (ev.dataTransfer.items) { + for (let i = 0; i < ev.dataTransfer.items.length; i++) { + if (ev.dataTransfer.items[i].kind === 'file') { + const file = ev.dataTransfer.items[i].getAsFile(); + files.push(file); } - handleClickUpload(files); + } + } else { + for (let i = 0; i < ev.dataTransfer.files.length; i++) { + files.push(ev.dataTransfer.files[i]); + } } + handleClickUpload(files); + } - const clip = new Clipboard('.home-files .col--name', { - text: (trigger) => { - const filename = trigger.closest('[data-filename]').getAttribute('data-filename'); - return new URL(`/file/${UserContext._id}/${filename}`, window.location.href).toString(); - }, - }); - clip.on('success', () => { - Notification.success(i18n('Download link copied to clipboard!'), 1000); - }); - clip.on('error', () => { - Notification.error(i18n('Copy failed :(')); - }); - $(document).on('click', '.home-files .col--name', (ev) => ev.preventDefault()); - $(document).on('click', '[name="upload_file"]', () => handleClickUpload()); - $(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected()); - $(document).on('dragover', '.home-files', (ev) => handleDragOver(ev)); - $(document).on('drop', '.home-files', (ev) => handleDrop(ev)); + const clip = new Clipboard('.home-files .col--name', { + text: (trigger) => { + const filename = trigger.closest('[data-filename]').getAttribute('data-filename'); + return new URL(`/file/${UserContext._id}/${filename}`, window.location.href).toString(); + }, + }); + clip.on('success', () => { + Notification.success(i18n('Download link copied to clipboard!'), 1000); + }); + clip.on('error', () => { + Notification.error(i18n('Copy failed :(')); + }); + $(document).on('click', '.home-files .col--name', (ev) => ev.preventDefault()); + $(document).on('click', '[name="upload_file"]', () => handleClickUpload()); + $(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected()); + $(document).on('dragover', '.home-files', (ev) => handleDragOver(ev)); + $(document).on('drop', '.home-files', (ev) => handleDrop(ev)); }); export default page; diff --git a/packages/ui-default/pages/problem_submit.page.js b/packages/ui-default/pages/problem_submit.page.js index 9ba67db3..2315525d 100644 --- a/packages/ui-default/pages/problem_submit.page.js +++ b/packages/ui-default/pages/problem_submit.page.js @@ -7,7 +7,7 @@ function setOptions($el, options) { }); } -const page = new NamedPage(['problem_submit', 'contest_detail_problem_submit', 'homework_detail_problem_submit'], async (name) => { +const page = new NamedPage(['problem_submit', 'contest_detail_problem_submit', 'homework_detail_problem_submit'], async () => { $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => { $(ev.currentTarget).hide(); $('[name="problem-sidebar__categories"]').show(); diff --git a/packages/ui-default/postcss.config.js b/packages/ui-default/postcss.config.js index a3db8ae9..a47ef4f9 100644 --- a/packages/ui-default/postcss.config.js +++ b/packages/ui-default/postcss.config.js @@ -1,5 +1,5 @@ module.exports = { - plugins: { - autoprefixer: {}, - }, + plugins: { + autoprefixer: {}, + }, }; diff --git a/packages/ui-default/tool/checkTranslation.js b/packages/ui-default/tool/checkTranslation.js deleted file mode 100644 index 5cd2844d..00000000 --- a/packages/ui-default/tool/checkTranslation.js +++ /dev/null @@ -1,106 +0,0 @@ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-unused-vars */ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable no-eval */ -const fs = require('fs'); -const path = require('path'); -const yaml = require('js-yaml'); - -const name = process.argv[2]; - -const IGNORE_CHECK = ['en']; -const IGNORE_MISSING = [ - '?', 'AC', 'ID', 'MD5', 'URL', -]; -const RE_TEMPLATE = /_\(['"]([\s\S])*?['"]\)/gmi; -const RE_UI = /i18n\(['"]([\s\S])*?['"](,.*?)?\)/gmi; -const TEMPLATE_ROOT = path.resolve(__dirname, '..', 'templates'); -const LOCALE_ROOT = path.resolve(__dirname, '..', 'locales'); -const texts = {}; -const result = {}; -const locales = fs.readdirSync(LOCALE_ROOT); -let currentFile = ''; - -function _(str, format) { - if (!texts[str]) texts[str] = [currentFile]; - else texts[str].push(currentFile); -} -const i18n = _; // lgtm [js/unused-local-variable] -const Setting = _; // lgtm [js/unused-local-variable] - -function scanTemplate(folder, relative = 'templates') { - const files = fs.readdirSync(folder); - for (const file of files) { - const p = path.join(folder, file); - if (fs.statSync(p).isDirectory()) { - scanTemplate(p, path.join(relative, file)); - } else { - currentFile = path.join(relative, file); - const f = fs.readFileSync(p).toString(); - f.replace(RE_TEMPLATE, (substr) => { - try { - // eslint-disable-next-line no-eval - eval(substr); - } catch (e) { - console.error('Cannot parse: ', substr, ' in file ', p); - } - }); - } - } -} -function scanUi(folder, relative = '') { - const files = fs.readdirSync(folder); - for (const file of files) { - const p = path.join(folder, file); - if (fs.statSync(p).isDirectory()) { - scanUi(p, path.join(relative, file)); - } else { - currentFile = path.join(relative, file); - const f = fs.readFileSync(p).toString(); - f.replace(RE_UI, (substr) => { - try { - eval(substr); - } catch (e) { - if (e.message.endsWith('is not defined')) { - global[e.message.split(' ')[0]] = () => { }; - try { - eval(substr); - } catch (err) { - console.error('Cannot parse: ', substr, ' in file ', p); - } - } else console.error('Cannot parse: ', substr, ' in file ', p); - } - }); - } - } -} -scanTemplate(TEMPLATE_ROOT); -scanUi(path.join(process.cwd(), 'components')); -scanUi(path.join(process.cwd(), 'pages')); -scanUi(path.join(process.cwd(), 'misc')); -if (!name) { - for (const locale of locales) { - if (!IGNORE_CHECK.includes(locale.split('.')[0])) { - const p = path.join(LOCALE_ROOT, locale); - const f = fs.readFileSync(p).toString(); - const l = yaml.safeLoad(f); - for (const str in texts) { - if (!l[str]) { - if (result[str]) result[str].locale.push(locale); - else result[str] = { source: texts[str], locale: [locale] }; - } - } - } - } -} else { - const p = path.join(LOCALE_ROOT, name); - const f = fs.readFileSync(p).toString(); - const l = yaml.safeLoad(f); - for (const str in texts) { - if (!l[str]) result[str] = texts[str]; - } -} -for (const str of IGNORE_MISSING) delete result[str]; -console.log(`${Object.keys(result).length} translations missing.`); -fs.writeFileSync(path.join(__dirname, '..', '__result.json'), JSON.stringify(result, null, 2)); -console.log(`Result wrote to ${path.resolve(__dirname, '..', '__result.json')}`); diff --git a/packages/ui-default/utils/base64.js b/packages/ui-default/utils/base64.js index 22535703..30229170 100644 --- a/packages/ui-default/utils/base64.js +++ b/packages/ui-default/utils/base64.js @@ -1,104 +1,104 @@ const Base64 = { - _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', + _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', - encode(input) { - let output = ''; - let chr1; - let chr2; - let chr3; - let enc1; - let enc2; - let enc3; - let enc4; - let i = 0; - input = Base64._utf8_encode(input); - while (i < input.length) { - chr1 = input.charCodeAt(i++); - chr2 = input.charCodeAt(i++); - chr3 = input.charCodeAt(i++); - enc1 = chr1 >> 2; - enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; - if (Number.isNaN(chr2)) enc3 = enc4 = 64; - else if (Number.isNaN(chr3)) enc4 = 64; - output = output + encode(input) { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let i = 0; + input = Base64._utf8_encode(input); + while (i < input.length) { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + if (Number.isNaN(chr2)) enc3 = enc4 = 64; + else if (Number.isNaN(chr3)) enc4 = 64; + output = output + this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4); - } - return output; - }, + } + return output; + }, - decode(input) { - let output = ''; - let chr1; - let chr2; - let chr3; - let enc1; - let enc2; - let enc3; - let enc4; - let i = 0; - input = input.replace(/[^A-Za-z0-9+/=]/g, ''); - while (i < input.length) { - enc1 = this._keyStr.indexOf(input.charAt(i++)); - enc2 = this._keyStr.indexOf(input.charAt(i++)); - enc3 = this._keyStr.indexOf(input.charAt(i++)); - enc4 = this._keyStr.indexOf(input.charAt(i++)); - chr1 = (enc1 << 2) | (enc2 >> 4); - chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); - chr3 = ((enc3 & 3) << 6) | enc4; - output += String.fromCharCode(chr1); - if (enc3 !== 64) output += String.fromCharCode(chr2); - if (enc4 !== 64) output += String.fromCharCode(chr3); - } - output = Base64._utf8_decode(output); - return output; - }, + decode(input) { + let output = ''; + let chr1; + let chr2; + let chr3; + let enc1; + let enc2; + let enc3; + let enc4; + let i = 0; + input = input.replace(/[^A-Za-z0-9+/=]/g, ''); + while (i < input.length) { + enc1 = this._keyStr.indexOf(input.charAt(i++)); + enc2 = this._keyStr.indexOf(input.charAt(i++)); + enc3 = this._keyStr.indexOf(input.charAt(i++)); + enc4 = this._keyStr.indexOf(input.charAt(i++)); + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + output += String.fromCharCode(chr1); + if (enc3 !== 64) output += String.fromCharCode(chr2); + if (enc4 !== 64) output += String.fromCharCode(chr3); + } + output = Base64._utf8_decode(output); + return output; + }, - _utf8_encode(string) { - string = string.replace(/\r\n/g, '\n'); - let utftext = ''; - for (let n = 0; n < string.length; n++) { - const c = string.charCodeAt(n); - if (c < 128) { - utftext += String.fromCharCode(c); - } else if ((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } - } - return utftext; - }, + _utf8_encode(string) { + string = string.replace(/\r\n/g, '\n'); + let utftext = ''; + for (let n = 0; n < string.length; n++) { + const c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } else if ((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + }, - _utf8_decode(utftext) { - let string = ''; - let i = 0; - let c = 0; - let c2 = 0; - let c3 = 0; - while (i < utftext.length) { - c = utftext.charCodeAt(i); - if (c < 128) { - string += String.fromCharCode(c); - i++; - } else if ((c > 191) && (c < 224)) { - c2 = utftext.charCodeAt(i + 1); - string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); - i += 2; - } else { - c2 = utftext.charCodeAt(i + 1); - c3 = utftext.charCodeAt(i + 2); - string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); - i += 3; - } - } - return string; - }, + _utf8_decode(utftext) { + let string = ''; + let i = 0; + let c = 0; + let c2 = 0; + let c3 = 0; + while (i < utftext.length) { + c = utftext.charCodeAt(i); + if (c < 128) { + string += String.fromCharCode(c); + i++; + } else if ((c > 191) && (c < 224)) { + c2 = utftext.charCodeAt(i + 1); + string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); + i += 2; + } else { + c2 = utftext.charCodeAt(i + 1); + c3 = utftext.charCodeAt(i + 2); + string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); + i += 3; + } + } + return string; + }, }; export default Base64; diff --git a/packages/ui-default/utils/cloneAttributes.js b/packages/ui-default/utils/cloneAttributes.js index 2748600d..e7760a8a 100644 --- a/packages/ui-default/utils/cloneAttributes.js +++ b/packages/ui-default/utils/cloneAttributes.js @@ -1,6 +1,6 @@ export default function cloneAttributes(from, to) { - const attributes = from.prop('attributes'); - $.each(attributes, function () { - to.attr(this.name, this.value); - }); + const attributes = from.prop('attributes'); + $.each(attributes, function () { + to.attr(this.name, this.value); + }); } diff --git a/packages/ui-default/utils/request.js b/packages/ui-default/utils/request.js index 849bd691..5d76634b 100644 --- a/packages/ui-default/utils/request.js +++ b/packages/ui-default/utils/request.js @@ -1,5 +1,4 @@ import i18n from './i18n'; -import substitute from './substitute'; const request = { /** @@ -24,7 +23,7 @@ const request = { } else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) { const { error } = jqXHR.responseJSON; if (error.params) { - const message = substitute(error.message, ...error.params); + const message = i18n(error.message, ...error.params); const err = new Error(message); err.rawMessage = error.message; err.params = error.params;