ui-default: init

将ui-default合并至主存储库便于维护
This commit is contained in:
undefined committed 2021-04-03 01:44:22 +08:00
1 parent 6317c9057f
commit 5faa54c7ca
564 files changed
+23083 -14

No files matched your search

+6 -2
View File
@@ -25,9 +25,13 @@ jobs:
- name: Install - name: Install
run: yarn run: yarn
- name: Build - name: Build
run: yarn build run: |
yarn build
yarn build:ui:production
- name: Lint - name: Lint
run: yarn lint:ci run: |
yarn lint:ci
yarn lint:ui:ci
- name: Unit Test - name: Unit Test
run: yarn jest --runInBand run: yarn jest --runInBand
- name: Report Coverage - name: Report Coverage
+7
View File
@@ -2,6 +2,7 @@
.build/ .build/
dist/ dist/
*.cache *.cache
.cache
/build/*.js /build/*.js
.cache/ .cache/
*.log *.log
@@ -21,11 +22,17 @@ yarn.lock
tsconfig.json tsconfig.json
# Project Rules # Project Rules
## Core
packages/**/*.js packages/**/*.js
!/build/build.js !/build/build.js
!**/bin/*.js !**/bin/*.js
!*.page.js !*.page.js
!/install/jssh.d.ts !/install/jssh.d.ts
## ui-default
!packages/ui-default/**/*.js
packages/ui-default/public
packages/ui-default/misc/.iconfont
packages/ui-default/static/locale
# Data files # Data files
*.mmdb *.mmdb
+1
View File
@@ -5,3 +5,4 @@ vscode:
extensions: extensions:
- dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw== - dbaeumer.vscode-eslint@2.1.3:1NRvj3UKNTNwmYjptmUmIw==
- vscode-icons-team.vscode-icons@10.2.0:DrQUJPtFm0woSFiyitha8Q== - vscode-icons-team.vscode-icons@10.2.0:DrQUJPtFm0woSFiyitha8Q==
- samuelcolvin.jinjahtml@0.12.0:lFI6zKplznEZPdWvFvRHwg==
+3 -1
View File
@@ -1,6 +1,8 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"gruntfuggly.todo-tree" "gruntfuggly.todo-tree",
"ronnidc.nunjucks",
"sysoev.language-stylus"
] ]
} }
+1 -2
View File
@@ -2,8 +2,7 @@
"files.autoGuessEncoding": false, "files.autoGuessEncoding": false,
"files.encoding": "utf8", "files.encoding": "utf8",
"files.eol": "\n", "files.eol": "\n",
"editor.detectIndentation": false, "editor.detectIndentation": true,
"editor.tabSize": 4,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.renderWhitespace": "boundary", "editor.renderWhitespace": "boundary",
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
+1
View File
@@ -49,6 +49,7 @@ const configFlat = {
const packages = fs.readdirSync(path.resolve(process.cwd(), 'packages')); const packages = fs.readdirSync(path.resolve(process.cwd(), 'packages'));
console.log(packages); console.log(packages);
for (const package of packages) { for (const package of packages) {
if (package === 'ui-default') continue;
const basedir = path.resolve(process.cwd(), 'packages', package); const basedir = path.resolve(process.cwd(), 'packages', package);
const files = fs.readdirSync(basedir); const files = fs.readdirSync(basedir);
if (!files.includes('src') && !files.map(n => n.split('.')[1]).includes('ts')) continue; if (!files.includes('src') && !files.map(n => n.split('.')[1]).includes('ts')) continue;
+1 -1
View File
@@ -15,7 +15,7 @@ export function getWorkspaces() {
}); });
} }
export type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' export type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';
export interface PackageJson extends Partial<Record<DependencyType, Record<string, string>>> { export interface PackageJson extends Partial<Record<DependencyType, Record<string, string>>> {
name?: string name?: string
+6
View File
@@ -8,8 +8,14 @@
"scripts": { "scripts": {
"build": "node build/build.js", "build": "node build/build.js",
"build:watch": "node build/build.js --watch", "build:watch": "node build/build.js --watch",
"build:ui": "node packages/ui-default/build",
"build:ui:watch": "node packages/ui-default/build --watch",
"build:ui:production": "node packages/ui-default/build --production",
"lint": "eslint packages --ext ts --fix", "lint": "eslint packages --ext ts --fix",
"lint:ci": "eslint packages --ext ts", "lint:ci": "eslint packages --ext ts",
"lint:ui": "yarn run --cwd=packages/ui-default eslint . --fix",
"lint:ui:ci": "yarn run --cwd=packages/ui-default eslint .",
"snowpack": "cd packages/ui-default && snowpack dev",
"pub": "node build/publish", "pub": "node build/publish",
"jest": "node build/jest", "jest": "node build/jest",
"debug": "node --async-stack-traces --trace-deprecation --enable-source-maps build/start --debug", "debug": "node --async-stack-traces --trace-deprecation --enable-source-maps build/start --debug",
+1 -8
View File
@@ -5,17 +5,10 @@
"repository": "git@github.com:hydro-dev/Hydro.git", "repository": "git@github.com:hydro-dev/Hydro.git",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "MIT", "license": "MIT",
"scripts": {
"lint": "eslint lib.ts service.ts --fix",
"build": "tsc"
},
"peerDependencies": {
"hydrooj": "^2.15.0"
},
"dependencies": { "dependencies": {
"ws": "^7.4.4" "ws": "^7.4.4"
}, },
"devDependencies": { "devDependencies": {
"@types/ws": "^7.4.0" "@types/ws": "^7.4.1"
} }
} }
+4
View File
@@ -0,0 +1,4 @@
node_modules
backendlib
build
public
+113
View File
@@ -0,0 +1,113 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const path = require('path');
module.exports = {
root: true,
parser: '@babel/eslint-parser',
plugins: ['babel', 'react'],
env: {
browser: true,
es6: true,
jquery: true,
commonjs: true,
},
extends: ['airbnb'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 7,
ecmaFeatures: {
impliedStrict: true,
experimentalObjectRestSpread: true,
jsx: true,
legacyDecorators: true,
},
babelOptions: {
configFile: './babel.config.js',
},
},
settings: {
'import/resolver': {
webpack: {
config: {
resolve: {
extensions: ['.js', '.jsx'],
alias: {
vj: path.resolve(__dirname),
},
},
},
},
},
},
globals: {
__webpack_public_path__: true,
__webpack_require__: true,
UiContext: true,
UserContext: true,
LOCALES: true,
window: true,
},
rules: {
// FIXME A bug with eslint-parser
'template-curly-spacing': 'off',
indent: 'off',
'comma-dangle': [
'error',
'always-multiline',
],
// indent: [
// 'error',
// 2,
// {
// SwitchCase: 0,
// },
// ],
'max-len': ['error', 150],
quotes: 'warn',
'class-methods-use-this': 'off',
'consistent-return': 'warn',
'func-names': 'off',
'import/first': 'off',
'import/no-extraneous-dependencies': 'off',
'max-classes-per-file': 'off',
'no-unused-vars': 'warn',
'no-console': 'off',
'no-continue': 'off',
'no-mixed-operators': 'off',
'no-multi-assign': 'off',
'no-plusplus': 'off',
'no-restricted-syntax': [
'error',
{
selector: 'LabeledStatement',
message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
},
{
selector: 'WithStatement',
message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
},
],
'no-underscore-dangle': 'off',
'no-await-in-loop': 'off',
'no-lonely-if': 'off',
'no-param-reassign': 'off',
'no-bitwise': 'off',
'react/prefer-stateless-function': 'off',
'react/static-property-placement': 'off',
'react/self-closing-comp': 'off',
'react/prop-types': 'off',
'react/jsx-filename-extension': 'off',
'react/jsx-one-expression-per-line': 'off',
'react/jsx-props-no-spreading': 'off',
'react/no-string-refs': 'off',
'react/require-default-props': 'off',
'react/react-in-jsx-scope': 'off',
'react/destructuring-assignment': 'off',
'react/button-has-type': 'off',
'react/forbid-prop-types': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-a11y/anchor-is-valid': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
},
};
+11
View File
@@ -0,0 +1,11 @@
node_modules/
.cache/
build/
common/
components/
constant/
misc/
pages/
static/
utils/
__*
+11
View File
@@ -0,0 +1,11 @@
# Hydro-UI-Default
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhydro-dev%2Fui-default.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhydro-dev%2Fui-default?ref=badge_shield)
Based on Vijos-UI-Framework
See [vijos/vj4/vj4/ui/README.md](https://github.com/vijos/vj4/blob/master/vj4/ui/README.md)
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhydro-dev%2Fui-default.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fhydro-dev%2Fui-default?ref=badge_large)
+45
View File
@@ -0,0 +1,45 @@
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',
],
plugins: [
'toolbar',
'line-numbers',
],
css: true,
},
],
],
presets: [
['@babel/preset-env', { loose: true, modules: false }],
'@babel/preset-react',
],
};
@@ -0,0 +1,136 @@
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
const katex = require('katex');
const logger = new global.Hydro.Logger('katex');
function isValidDelim(state, pos) {
const max = state.posMax;
const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;
const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
let canOpen = true;
let canClose = true;
if (prevChar === 0x09
||/* \t */ (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) canClose = false;
if (nextChar === 0x09/* \t */) canOpen = false;
return {
canOpen,
canClose,
};
}
function inline(state, silent) {
let pos;
if (state.src[state.pos] !== '$') return false;
let res = isValidDelim(state, state.pos);
if (!res.canOpen) {
if (!silent) state.pending += '$';
state.pos += 1;
return true;
}
const start = state.pos + 1;
let match = start;
// eslint-disable-next-line no-cond-assign
while ((match = state.src.indexOf('$', match)) !== -1) {
pos = match - 1;
while (state.src[pos] === '\\') pos -= 1;
if ((match - pos) % 2) break;
match += 1;
}
if (match === -1) {
if (!silent) state.pending += '$';
state.pos = start;
return true;
}
if (match - start === 0) {
if (!silent) state.pending += '$$';
state.pos = start + 1;
return true;
}
res = isValidDelim(state, match);
if (!res.canClose) {
if (!silent) state.pending += '$';
state.pos = start;
return true;
}
if (!silent) {
const token = state.push('math_inline', 'math', 0);
token.markup = '$';
token.content = state.src.slice(start, match);
}
state.pos = match + 1;
return true;
}
function block(state, start, end, silent) {
let lastLine;
let lastPos;
let found = false;
let pos = state.bMarks[start] + state.tShift[start];
let max = state.eMarks[start];
if (pos + 2 > max) return false;
if (state.src.slice(pos, pos + 2) !== '$$') return false;
pos += 2;
let firstLine = state.src.slice(pos, max);
if (silent) return true;
if (firstLine.trim().slice(-2) === '$$') {
firstLine = firstLine.trim().slice(0, -2);
found = true;
}
let next = start;
while (!found) {
next++;
if (next >= end) break;
pos = state.bMarks[next] + state.tShift[next];
max = state.eMarks[next];
if (pos < max && state.tShift[next] < state.blkIndent) break;
if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
lastPos = state.src.slice(0, max).lastIndexOf('$$');
lastLine = state.src.slice(pos, lastPos);
found = true;
}
}
state.line = next + 1;
const token = state.push('math_block', 'math', 0);
token.block = true;
token.content = (firstLine && firstLine.trim() ? `${firstLine}\n` : '')
+ state.getLines(start + 1, next, state.tShift[start], true)
+ (lastLine && lastLine.trim() ? lastLine : '');
token.map = [start, state.line];
token.markup = '$$';
return true;
}
module.exports = function plugin(md) {
const options = { throwOnError: false, strict: false };
const katexInline = function (latex) {
options.displayMode = false;
try {
return katex.renderToString(latex, options);
} catch (error) {
if (options.throwOnError) logger.error(error);
return latex;
}
};
const inlineRenderer = function (tokens, idx) {
return katexInline(tokens[idx].content);
};
const katexBlock = function (latex) {
options.displayMode = true;
try {
return `<p>${katex.renderToString(latex, options)}</p>`;
} catch (error) {
if (options.throwOnError) logger.error(error);
return latex;
}
};
const blockRenderer = function (tokens, idx) {
return `${katexBlock(tokens[idx].content)}\n`;
};
md.inline.ruler.after('escape', 'math_inline', inline);
md.block.ruler.after('blockquote', 'math_block', block, {
alt: ['paragraph', 'reference', 'blockquote', 'list'],
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
@@ -0,0 +1,161 @@
/* eslint-disable max-len */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-restricted-properties */
const ytRegex = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
function youtubeParser(url) {
const match = url.match(ytRegex);
return match && match[7].length === 11 ? match[7] : url;
}
const vimeoRegex = /https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?)/;
function vimeoParser(url) {
const match = url.match(vimeoRegex);
return match && typeof match[3] === 'string' ? match[3] : url;
}
const vineRegex = /^http(?:s?):\/\/(?:www\.)?vine\.co\/v\/([a-zA-Z0-9]{1,13}).*/;
function vineParser(url) {
const match = url.match(vineRegex);
return match && match[1].length === 11 ? match[1] : url;
}
const preziRegex = /^https:\/\/prezi.com\/(.[^/]+)/;
function preziParser(url) {
const match = url.match(preziRegex);
return match ? match[1] : url;
}
// TODO: Write regex for staging and local servers.
const mfrRegex = /^http(?:s?):\/\/(?:www\.)?mfr\.osf\.io\/render\?url=http(?:s?):\/\/osf\.io\/([a-zA-Z0-9]{1,5})\/\?action=download/;
function mfrParser(url) {
const match = url.match(mfrRegex);
return match ? match[1] : url;
}
const EMBED_REGEX = /@\[([a-zA-Z].+)]\((.*?)[)]/im;
function extractVideoParameters(url) {
const parameterMap = new Map();
const params = url.replace(/&amp;/gi, '&').split(/[#?&]/);
if (params.length > 1) {
for (let i = 1; i < params.length; i += 1) {
const keyValue = params[i].split('=');
if (keyValue.length > 1) parameterMap.set(keyValue[0], keyValue[1]);
}
}
return parameterMap;
}
function videoUrl(service, videoID, url, options) {
switch (service) {
case 'youtube': {
const parameters = extractVideoParameters(url);
if (options.youtube.parameters) {
Object.keys(options.youtube.parameters).forEach((key) => {
parameters.set(key, options.youtube.parameters[key]);
});
}
const timeParameter = parameters.get('t');
if (timeParameter !== undefined) {
let startTime = 0;
const timeParts = timeParameter.match(/[0-9]+/g);
let j = 0;
while (timeParts.length > 0) {
startTime += Number(timeParts.pop()) * Math.pow(60, j);
j += 1;
}
parameters.set('start', startTime);
parameters.delete('t');
}
parameters.delete('v');
parameters.delete('feature');
parameters.delete('origin');
const parameterArray = Array.from(parameters, (p) => p.join('='));
const parameterPos = videoID.indexOf('?');
let finalUrl = 'https://www.youtube';
if (options.youtube.nocookie || url.indexOf('youtube-nocookie.com') > -1) finalUrl += '-nocookie';
finalUrl += `.com/embed/${parameterPos > -1 ? videoID.substr(0, parameterPos) : videoID}`;
if (parameterArray.length > 0) finalUrl += `?${parameterArray.join('&')}`;
return finalUrl;
}
case 'vimeo':
return `https://player.vimeo.com/video/${videoID}`;
case 'vine':
return `https://vine.co/v/${videoID}/embed/${options.vine.embed}`;
case 'prezi':
return `https://prezi.com/embed/${videoID
}/?bgcolor=ffffff&amp;lock_to_path=0&amp;autoplay=0&amp;autohide_ctrls=0&amp;`
+ 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&amp;'
+ 'landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI';
case 'osf':
return `https://mfr.osf.io/render?url=https://osf.io/${videoID}/?action=download`;
default:
return service;
}
}
module.exports = function videoPlugin(md) {
const options = {
url: videoUrl,
youtube: { width: 640, height: 390, nocookie: false },
vimeo: { width: 500, height: 281 },
vine: { width: 600, height: 600, embed: 'simple' },
prezi: { width: 550, height: 400 },
osf: { width: '100%', height: '100%' },
pdf: videoUrl,
};
md.renderer.rules.video = function tokenizeReturn(tokens, idx) {
const videoID = md.utils.escapeHtml(tokens[idx].videoID);
const service = md.utils.escapeHtml(tokens[idx].service).toLowerCase();
const checkUrl = /http(?:s?):\/\/(?:www\.)?[a-zA-Z0-9-:.]{1,}\/render(?:\/)?[a-zA-Z0-9.&;?=:%]{1,}url=http(?:s?):\/\/[a-zA-Z0-9 -:.]{1,}\/[a-zA-Z0-9]{1,5}\/\?[a-zA-Z0-9.=:%]{1,}/;
let num;
if (service === 'osf' && videoID) {
num = Math.random() * 0x10000;
if (videoID.match(checkUrl)) {
return `<div id="${num}" class="mfr mfr-file"></div><script>`
+ `$(document).ready(function () {new mfr.Render("${num}", "${videoID}");`
+ ' }); </script>';
}
return `<div id="${num}" class="mfr mfr-file"></div><script>`
+ `$(document).ready(function () {new mfr.Render("${num}", "https://mfr.osf.io/`
+ `render?url=https://osf.io/${videoID}/?action=download%26mode=render");`
+ ' }); </script>';
}
if (service === 'pdf') return `<iframe src="${videoID}?noDisposition=on#view=fit" width="100%" style="min-height: 100vh;border: none;"></iframe>`;
return videoID === '' ? ''
: `<div class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item ${service}-player" type="text/html" width="${options[service].width
}" height="${options[service].height
}" src="${options.url(service, videoID, tokens[idx].url, options)
}" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>`;
};
md.inline.ruler.before('emphasis', 'video', (state, silent) => {
let token;
let videoID;
const theState = state;
const oldPos = state.pos;
if (state.src.charCodeAt(oldPos) !== 0x40
||/* @ */ state.src.charCodeAt(oldPos + 1) !== 0x5B/* [ */) {
return false;
}
const match = EMBED_REGEX.exec(state.src.slice(state.pos, state.src.length));
if (!match || match.length < 3) return false;
const service = match[1];
videoID = match[2];
const serviceLower = service.toLowerCase();
if (serviceLower === 'youtube') videoID = youtubeParser(videoID);
else if (serviceLower === 'vimeo') videoID = vimeoParser(videoID);
else if (serviceLower === 'vine') videoID = vineParser(videoID);
else if (serviceLower === 'prezi') videoID = preziParser(videoID);
else if (serviceLower === 'osf') videoID = mfrParser(videoID);
else if (!options[serviceLower]) return false;
if (videoID === ')') videoID = '';
const serviceStart = oldPos + 2;
const serviceEnd = md.helpers.parseLinkLabel(state, oldPos + 1, false);
if (!silent) {
theState.pos = serviceStart;
theState.service = theState.src.slice(serviceStart, serviceEnd);
const newState = new theState.md.inline.State(service, theState.md, theState.env, []);
newState.md.inline.tokenize(newState);
token = theState.push('video', '');
token.videoID = videoID;
token.service = service;
token.url = match[2];
token.level = theState.level;
}
theState.pos += theState.src.indexOf(')', theState.pos);
return true;
});
};
@@ -0,0 +1,73 @@
const MarkdownIt = require('markdown-it');
const Prism = require('prismjs');
// For math: $a\times b\eq 10$
// Specific image size: ![image](image.png =100x100)
const Imsize = require('markdown-it-imsize');
/* Footnote support.
Here is a footnote reference,[^1] and another.[^longnote]
[^1]: Here is the footnote.
[^longnote]: Here's one with multiple blocks.
Subsequent paragraphs are indented to show that they
belong to the previous footnote.
*/
const Footnote = require('markdown-it-footnote');
// ==Highlight==
const Mark = require('markdown-it-mark');
const TOC = require('markdown-it-table-of-contents');
const Anchor = require('markdown-it-anchor');
// @[url](videourl)
// @[youtube](https://youtube.com/watch?v=xxx)
// @[pdf](https://foo.com/bar.pdf)
const Media = require('./markdown-it-media');
const Katex = require('./markdown-it-katex');
require('prismjs/components/index');
class Markdown extends MarkdownIt {
constructor() {
super({
linkify: true,
highlight(str, lang) {
if (lang && Prism.languages[lang]) {
try {
return Prism.highlight(str, Prism.languages[lang], lang);
} catch (__) { } // eslint-disable-line no-empty
}
return '';
},
});
this.linkify.tlds('.py', false);
this.use(Media);
this.use(Imsize);
this.use(Footnote);
this.use(Mark);
this.use(Anchor);
this.use(TOC);
this.use(Katex);
}
}
const md = new Markdown();
function plugin(func, ...args) {
md.use(func, ...args);
}
function render(text, html = false) {
md.set({ html: !!html });
return md.render(text);
}
function renderInline(text, html = false) {
md.set({ html: !!html });
return md.renderInline(text);
}
// eslint-disable-next-line no-multi-assign
global.Hydro.lib.markdown = module.exports = {
md, plugin, render, renderInline,
};
+206
View File
@@ -0,0 +1,206 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const serialize = require('serialize-javascript');
const nunjucks = require('nunjucks');
const { filter } = require('lodash');
const { argv } = require('yargs');
const Xss = require('xss');
const markdown = require('./markdown');
const { misc, buildContent } = global.Hydro.lib;
const xss = new Xss.FilterXSS({
whiteList: {
a: ['target', 'href', 'title'],
abbr: ['title'],
address: [],
area: ['shape', 'coords', 'href', 'alt'],
article: [],
aside: [],
audio: ['autoplay', 'controls', 'loop', 'preload', 'src'],
b: [],
bdi: ['dir'],
bdo: ['dir'],
big: [],
blockquote: ['cite'],
br: [],
caption: [],
center: [],
cite: [],
code: [],
col: ['align', 'valign', 'span', 'width'],
colgroup: ['align', 'valign', 'span', 'width'],
dd: [],
del: ['datetime'],
details: ['open'],
div: [],
dl: [],
dt: [],
em: [],
font: ['color', 'size', 'face'],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
header: [],
hr: [],
i: [],
img: ['src', 'alt', 'title', 'width', 'height'],
ins: ['datetime'],
li: [],
mark: [],
ol: [],
p: [],
pre: [],
s: [],
section: [],
small: [],
span: ['class'],
sub: [],
sup: [],
strong: [],
table: ['width', 'border', 'align', 'valign'],
tbody: ['align', 'valign'],
td: ['width', 'rowspan', 'colspan', 'align', 'valign'],
tfoot: ['align', 'valign'],
th: ['width', 'rowspan', 'colspan', 'align', 'valign'],
thead: ['align', 'valign'],
tr: ['rowspan', 'align', 'valign'],
tt: [],
u: [],
ul: [],
video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width'],
},
});
class Loader extends nunjucks.Loader {
// eslint-disable-next-line class-methods-use-this
getSource(name) {
if (!argv.template) {
if (!global.Hydro.ui.template[name]) throw new Error(`Cannot get template ${name}`);
return {
src: global.Hydro.ui.template[name],
path: name,
noCache: false,
};
}
let fullpath = null;
const p = path.resolve(process.cwd(), argv.template, name);
if (fs.existsSync(p)) fullpath = p;
if (!fullpath) throw new Error(`Cannot get template ${name}`);
return {
src: fs.readFileSync(fullpath, 'utf-8').toString(),
path: fullpath,
noCache: true,
};
}
}
class Nunjucks extends nunjucks.Environment {
constructor() {
super(new Loader(), { autoescape: true, trimBlocks: true });
this.addFilter('json', (self) => JSON.stringify(self));
this.addFilter('parseYaml', (self) => yaml.load(self));
this.addFilter('dumpYaml', (self) => yaml.dump(self));
this.addFilter('xss', (self) => xss.process(self));
this.addFilter('serialize', (self, ignoreFunction = true) => serialize(self, { ignoreFunction }));
this.addFilter('assign', (self, data) => Object.assign(self, data));
this.addFilter('markdown', (self, html = false) => markdown.render(self, html));
this.addFilter('markdownInline', (self, html = false) => markdown.renderInline(self, html));
this.addFilter('ansi', (self) => misc.ansiToHtml(self));
this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64'));
this.addFilter('base64_decode', (s) => Buffer.from(s, 'base64').toString());
this.addFilter('bitand', (self, val) => self & val);
this.addFilter('toString', (self) => (typeof self === 'string' ? self : JSON.stringify(self)));
this.addFilter('content', (content, language, html) => {
let s = '';
try {
s = JSON.parse(content);
} catch {
s = content;
}
if (typeof s === 'object' && !(s instanceof Array)) {
if (s[language]) s = s[language];
else s = s[Object.keys(s)[0]];
}
if (s instanceof Array) s = buildContent(s, html ? 'html' : 'markdown', (str) => str.translate(language));
return html ? xss.process(s) : markdown.render(s);
});
this.addFilter('log', (self) => {
console.log(self);
return self;
});
}
}
nunjucks.runtime.memberLookup = function memberLookup(obj, val) {
if ((obj || {})._original) obj = obj._original;
if (obj === undefined || obj === null) return undefined;
if (typeof obj[val] === 'function') {
const fn = function () {
// eslint-disable-next-line
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
// eslint-disable-next-line prefer-rest-params
args[_key2] = arguments[_key2];
}
// eslint-disable-next-line block-scoped-var
return obj[val].call(obj, ...args);
};
fn._original = obj[val];
return fn;
}
return obj[val];
};
const env = new Nunjucks();
env.addGlobal('static_url', (assetName) => {
const cdnPrefix = global.Hydro.model.system.get('server.cdn');
if (global.Hydro.ui.manifest[assetName]) {
return `${cdnPrefix}${global.Hydro.ui.manifest[assetName]}`;
}
return `${cdnPrefix}${assetName}`;
});
// eslint-disable-next-line no-eval
env.addGlobal('eval', eval);
env.addGlobal('typeof', (o) => typeof o);
env.addGlobal('datetimeSpan', misc.datetimeSpan);
env.addGlobal('paginate', misc.paginate);
env.addGlobal('size', misc.size);
env.addGlobal('gravatar', misc.gravatar);
env.addGlobal('model', global.Hydro.model);
env.addGlobal('ui', global.Hydro.ui);
env.addGlobal('isIE', (str) => str.includes('MSIE') || str.includes('rv:11.0'));
env.addGlobal('set', (obj, key, val) => {
if (val !== undefined) obj[key] = val;
else obj = { ...obj, key };
return '';
});
env.addGlobal('findSubModule', (prefix) => {
filter(Object.keys(global.Hydro.ui.template), (n) => n.startsWith(prefix));
});
async function render(name, state) {
// eslint-disable-next-line no-return-await
return await new Promise((resolve, reject) => {
env.render(name, {
page_name: name.split('.')[0],
...state,
formatJudgeTexts: (texts) => texts.map((text) => {
if (typeof text === 'string') return text;
return state._(text.message).format(...text.params || []) + ((argv.debug && text.stack) ? `\n${text.stack}` : '');
}).join('\n'),
perm: global.Hydro.model.builtin.PERM,
PRIV: global.Hydro.model.builtin.PRIV,
STATUS: global.Hydro.model.builtin.STATUS,
UiContext: state.handler.UiContext,
}, (err, res) => {
if (err) reject(err);
else resolve(res);
});
});
}
module.exports = render;
global.Hydro.lib.template = { render };
+5
View File
@@ -0,0 +1,5 @@
{
"mobile": 600,
"desktop": 1000,
"hd": 1250
}
+82
View File
@@ -0,0 +1,82 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable guard-for-in */
/* eslint-disable import/no-extraneous-dependencies */
import _ from 'lodash';
import moment from 'moment';
import gulp from 'gulp';
import log from 'fancy-log';
import chalk from 'chalk';
import svgmin from 'gulp-svgmin';
import vinylBuffer from 'vinyl-buffer';
import iconfont from 'gulp-iconfont';
import plumber from 'gulp-plumber';
import gulpif from 'gulp-if';
import nunjucks from '../plugins/gulpNunjucks';
import vjTouch from '../plugins/gulpTouch';
let isInWatchMode = false;
export const tasks = {};
const iconTimestamp = moment.utc([2020, 0, 1, 0, 0, 0, 0]).unix();
function handleWatchChange(name, r = 300) {
return _.debounce((ev) => {
log('File %s: %s', chalk.yellow(ev.type), ev.path);
tasks[name]();
}, r);
}
function offsetMtimeAtFirstBuild() {
// Offset the mtime at first build to
// workaround webpack/watchpack issue #25.
return gulpif(!isInWatchMode, vjTouch(~~((Date.now() - 30 * 1000) / 1000)));
}
export default function ({ errorHandler }) {
let iconfontTemplateArgs = null;
tasks['iconfont:template'] = () => gulp
.src('misc/icons/template/*.styl')
.pipe(nunjucks(iconfontTemplateArgs))
.pipe(gulp.dest('misc/.iconfont'))
.pipe(offsetMtimeAtFirstBuild());
tasks.iconfont = () => gulp
.src('misc/icons/*.svg')
.pipe(plumber({ errorHandler }))
.pipe(svgmin())
.pipe(gulp.dest('misc/icons'))
.pipe(offsetMtimeAtFirstBuild())
.pipe(iconfont({
fontHeight: 1000,
prependUnicode: false,
descent: 6.25 / 100 * 1000,
fontName: 'vj4icon',
formats: ['svg', 'ttf', 'eot', 'woff', 'woff2'],
timestamp: iconTimestamp,
}))
.on('glyphs', (glyphs, options) => {
iconfontTemplateArgs = { glyphs, options };
tasks['iconfont:template']();
})
.pipe(gulp.dest('misc/.iconfont'))
.pipe(vinylBuffer())
.pipe(offsetMtimeAtFirstBuild());
tasks.watch = () => {
isInWatchMode = true;
gulp.watch('misc/icons/*.svg', handleWatchChange('iconfont'));
};
for (const key in tasks) {
// gulp4 uses function name directly as task name
Object.defineProperty(tasks[key], 'name', {
value: key,
configurable: true,
});
tasks[key] = gulp.series(tasks[key]);
}
tasks.default = gulp.series(gulp.parallel(tasks.iconfont));
return tasks;
}
+177
View File
@@ -0,0 +1,177 @@
/* eslint-disable import/no-extraneous-dependencies */
import { dirname } from 'path';
import webpack from 'webpack';
import ExtractCssPlugin from 'mini-css-extract-plugin';
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
import FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin';
import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import SpeedMeasurePlugin from 'speed-measure-webpack-plugin';
import mapWebpackUrlPrefix from '../utils/mapWebpackUrlPrefix';
import StaticManifestPlugin from '../plugins/manifest.webpack';
import root from '../utils/root';
const beautifyOutputUrl = mapWebpackUrlPrefix([
{ prefix: 'node_modules/katex/dist/', replace: './katex/' },
{ prefix: 'misc/.iconfont', replace: './ui/iconfont' },
]);
const smp = new SpeedMeasurePlugin();
export default function (env = {}) {
function babelLoader() {
return {
loader: 'babel-loader',
options: {
...require(root('babel.config.js')), // eslint-disable-line
cacheDirectory: true,
},
};
}
function cssLoader() {
return {
loader: 'css-loader',
options: { importLoaders: 1 },
};
}
function postcssLoader() {
return {
loader: 'postcss-loader',
options: { sourceMap: env.production, config: { path: root('postcss.config.js') } },
};
}
function fileLoader() {
return {
loader: 'file-loader',
options: { name: '[path][name].[ext]?[sha1:hash:hex:10]' },
};
}
function extractCssLoader() {
return {
loader: ExtractCssPlugin.loader,
// FIXME auto?
options: {
publicPath: '',
},
};
}
const config = {
bail: true,
mode: env.production ? 'production' : 'development',
profile: true,
context: root(),
entry: {
hydro: './entry.js',
'theme.default': './theme/default.js',
},
output: {
path: root('public'),
publicPath: '/', // overwrite in entry.js
hashFunction: 'sha1',
hashDigest: 'hex',
hashDigestLength: 10,
filename: '[name].js?[chunkhash]',
chunkFilename: '[name].chunk.js?[chunkhash]',
},
resolve: {
modules: [root('node_modules'), root('../../node_modules')],
extensions: ['.js', '.jsx'],
alias: { vj: root() },
},
module: {
rules: [
{
test: /\.(svg|ttf|eot|woff|woff2|png|jpg|jpeg|gif)$/,
use: [fileLoader()],
},
{
test: /\.jsx?$/,
exclude: /node_modules[/\\]/,
use: [babelLoader()],
},
{
test: /\.styl$/,
use: [extractCssLoader(), cssLoader(), postcssLoader(), 'stylus-loader'],
},
{
test: /\.css$/,
use: [extractCssLoader(), cssLoader(), postcssLoader()],
},
],
},
optimization: {
splitChunks: {
minChunks: 2,
},
},
plugins: [
new webpack.ProgressPlugin(),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
katex: 'katex/dist/katex.js',
React: 'react',
}),
new ExtractCssPlugin({
filename: '[name].css?[contenthash:10]',
}),
new webpack.LoaderOptionsPlugin({
test: /\.styl$/,
stylus: {
default: {
preferPathResolver: 'webpack',
use: [require('rupture')()], // eslint-disable-line global-require
import: ['~vj/common/common.inc.styl'],
},
},
}),
new FriendlyErrorsPlugin(),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new CopyWebpackPlugin({
patterns: [
{ from: root('static') },
{ from: root(dirname(require.resolve('emojify.js/package.json')) + '/dist/images/basic'), to: 'img/emoji/' },
],
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: env.production ? '"production"' : '"debug"',
},
}),
...env.production
? [
new OptimizeCssAssetsPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.LoaderOptionsPlugin({ minimize: true }),
new webpack.HashedModuleIdsPlugin(),
]
: [new webpack.NamedModulesPlugin()],
new webpack.LoaderOptionsPlugin({
options: {
context: root(),
customInterpolateName: (url) => beautifyOutputUrl(url),
},
}),
new MonacoWebpackPlugin({
// available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
languages: ['cpp', 'csharp', 'java', 'javascript', 'python', 'rust', 'ruby', 'php', 'pascal', 'go'],
}),
new StaticManifestPlugin({
fileName: 'static-manifest.json',
ignore: [
'img/emoji/',
'katex/',
],
}),
...env.measure ? [new BundleAnalyzerPlugin({ analyzerPort: 'auto' })] : [],
],
};
return env.measure ? smp.wrap(config) : config;
}
+6
View File
@@ -0,0 +1,6 @@
require('@babel/register')({
"plugins": ["@babel/plugin-transform-runtime"],
"presets": [["@babel/preset-env", { "loose": true }]]
});
const main = require('./main.js');
if (!module.parent) main();
+62
View File
@@ -0,0 +1,62 @@
/* eslint-disable import/no-extraneous-dependencies */
import { argv } from 'yargs';
import webpack from 'webpack';
import gulp from 'gulp';
import log from 'fancy-log';
import chalk from 'chalk';
import root from './utils/root';
import gulpConfig from './config/gulp';
import webpackConfig from './config/webpack';
function runWebpack({ watch, production, measure }) {
const compiler = webpack(webpackConfig({ watch, production, measure }));
return new Promise((resolve, reject) => {
function compilerCallback(err, stats) {
if (err) {
console.error(err.stack || err);
if (err.details) console.error(err.details);
reject(err);
}
if (argv.detail) console.log(stats.toString());
if (!watch && (!stats || stats.hasErrors())) process.exitCode = 1;
resolve();
}
if (watch) compiler.watch({}, compilerCallback);
else compiler.run(compilerCallback);
});
}
async function runGulp() {
function handleError(err) {
log(chalk.red('Error: %s'), chalk.reset(err.toString() + err.stack));
if (err) process.exit(1);
}
const gulpTasks = gulpConfig({ production: true, errorHandler: handleError });
return new Promise((resolve) => {
const taskList = {};
gulp.on('start', ({ uid, name }) => {
log(chalk.blue('Starting task: %s'), chalk.reset(name));
taskList[uid] = true;
});
gulp.on('stop', ({ uid, name }) => {
log(chalk.green('Finished: %s'), chalk.reset(name));
taskList[uid] = false;
if (Object.values(taskList).filter((b) => b).length === 0) {
resolve();
}
});
gulpTasks.default();
});
}
async function main() {
const dir = process.cwd();
process.chdir(root());
await runGulp({});
await runWebpack(argv);
process.chdir(dir);
}
module.exports = main;
@@ -0,0 +1,27 @@
import through from 'through2';
import nunjucks from 'nunjucks';
export default function compile(data, options = {}) {
return through.obj(function (file, encoding, callback) {
if (file.isNull()) {
callback(null, file);
return;
}
const context = { ...data, ...file.data };
const filePath = file.path;
const env = options.env || new nunjucks.Environment(new nunjucks.FileSystemLoader(file.base), options);
if (options.filters && !options.env) {
for (const key of Object.keys(options.filters)) {
env.addFilter(key, options.filters[key]);
}
}
try {
file.contents = Buffer.from(env.renderString(file.contents.toString(), context));
file.extname = '.styl';
this.push(file);
} catch (error) {
this.emit('error', new Error(`gulp-nunjucks ${error} ${filePath}`));
}
callback();
});
}
@@ -0,0 +1,35 @@
/* eslint-disable import/no-extraneous-dependencies */
import PluginError from 'plugin-error';
import through from 'through2';
import fs from 'fs-extra';
export default function touch(mtime) {
async function touchFile(file) {
const fd = await fs.open(file.path, 'a');
try {
await fs.futimes(fd, mtime, mtime);
} finally {
await fs.close(fd);
}
}
function processStream(file, encoding, callback) {
if (file.isNull()) {
callback();
return;
}
if (file.isStream()) {
this.emit('error', new PluginError('gulpTouch', 'Stream not supported'));
callback();
return;
}
touchFile(file)
.catch((err) => this.emit('error', err))
.then(() => {
this.push(file);
callback();
});
}
return through.obj(processStream);
}
@@ -0,0 +1,49 @@
/* eslint-disable import/no-extraneous-dependencies */
const _ = require('lodash');
const crypto = require('crypto');
export default class StaticManifestPlugin {
constructor({ fileName, ignore }) {
this.fileName = fileName;
this.ignore = ignore;
}
apply(compiler) {
compiler.hooks.emit.tap('sync', (compilation) => {
const stats = compilation.getStats().toJson();
const manifest = _(stats.assets)
.map((asset) => {
const { name } = asset;
// Skip files listed in ignore
if (_.some(this.ignore, (ignorePattern) => name.indexOf(ignorePattern) > -1)) {
return null;
}
// Skip calculating hash for names like ?xxxx
if (name.indexOf('?') > -1) {
return [
name.replace(/\?[\s\S]*/, ''), // origin name
name, // name with hash
];
}
// We need to calculate hash for the remaining files.
const source = compilation.assets[asset.name].source();
const shasum = crypto.createHash('sha1');
shasum.update(source);
const hash = shasum.digest('hex').substr(0, 10);
return [
name, // origin name
`${name}?${hash}`, // name with hash
];
})
.filter()
.orderBy(['0'])
.fromPairs()
.value();
const json = JSON.stringify(manifest, null, 2);
compilation.assets[this.fileName] = {
source: () => json,
size: () => json.length,
};
});
}
}
@@ -0,0 +1,23 @@
module.exports = function () {
return {
name: 'provide-plugin',
async transform({ id, fileExt, contents }) {
if (fileExt !== '.js' && fileExt !== '.jsx') return contents;
if (id.includes('/node_modules/jquery.transit/')) {
return `import jQuery from 'jquery'\n${contents.replace('this, function', '{jQuery}, function')}`;
}
if (id.includes('/node_modules/sticky-kit/')) {
return `import jQuery from 'jquery'\n${contents.replace('$ = this.jQuery || window.jQuery;', '$ = jQuery;')}`;
}
if (id.includes('/node_modules/')) {
if (!/\/(jquery-scroll-lock|jquery\.easing|jquery\.transit)\//.test(id)) return { contents };
}
if (contents.includes('$(')) contents = `import $ from 'jquery';\n${contents}`;
if (contents.includes('jQuery')) contents = `import jQuery from 'jquery';\n${contents}`;
if (!contents.includes('import React')) {
if (contents.includes('React.') || fileExt === '.jsx') contents = `import React from 'react';\n${contents}`;
}
return contents.replace(/process\.env/g, 'import.meta.env');
},
};
};
@@ -0,0 +1,216 @@
const acorn = require('acorn');
const stage3 = require('acorn-stage3');
const walk = require('acorn-walk');
const astring = require('astring');
const path = require('path');
const fsp = require('fs').promises;
const dirWatchers = {};
const parser = acorn.Parser.extend(stage3);
async function requireContext(filePath) {
const source = await fsp.readFile(filePath, 'utf8');
if (!/\brequire\s*\.\s*context\s*\(/.test(source)) {
delete dirWatchers[filePath];
return null;
}
const ast = parser.parse(source,
{ sourceType: 'module', ecmaVersion: 'latest', locations: true });
const base = path.dirname(path.resolve(filePath));
const nodes = [];
walk.simple(ast, {
CallExpression(node) {
const { callee } = node;
const args = node.arguments;
if (callee.type !== 'MemberExpression') return;
if (callee.object.name !== 'require') return;
if (callee.property.name !== 'context') return;
if (args.length === 0) return;
if (!args.every((arg) => arg.type === 'Literal')) return;
if (args.length > 2 && !args[2].regex) return;
nodes.push(node);
},
});
if (nodes.length === 0) {
delete dirWatchers[filePath];
return null;
}
const imports = [];
const dirs = [];
await Promise.all(nodes.map(async (node) => {
const args = node.arguments;
const directory = path.resolve(base, args[0].value);
const recurse = args[1] && args[1].value;
const regExp = args[2] && new RegExp(args[2].regex.pattern, args[2].regex.flags);
dirs.push(directory);
async function getFiles(dir, recurse, pattern) {
try {
const dirents = await fsp.readdir(dir, { withFileTypes: true });
const files = await Promise.all(dirents.map((dirent) => {
const res = path.resolve(dir, dirent.name);
if (dirent.isDirectory()) {
return (recurse !== false) && getFiles(res);
}
return (!pattern || pattern.test(res)) ? res : null;
}));
return Array.prototype.concat(...files);
} catch (error) {
if (error.code === 'ENOENT') return [];
throw error;
}
}
const files = (await getFiles(directory, recurse, regExp))
.filter((file) => file).map((file) => {
file = path.relative(base, file);
if (!file.startsWith('/') && !file.startsWith('.')) file = `./${file}`;
return file;
});
const modules = files.map((file) => Math.random().toString(36).slice(2).replace(/\d/g, '') + file.split('/').pop().split('.')[0]
.replace(/^\w/, (c) => c.toUpperCase())
.replace(/[-_]\w/g, (c) => c[1].toUpperCase()).replace(/\W/g, '$'));
files.forEach((file, i) => {
imports.push({
type: 'ImportDeclaration',
specifiers: [
{
type: 'ImportDefaultSpecifier',
local: { type: 'Identifier', name: modules[i] },
},
],
source: { type: 'Literal', value: file, raw: JSON.stringify(file) },
});
});
const keys = files.map((file) => `./${path.relative(directory, path.resolve(base, file))}`);
const contextKeys = {
type: 'ArrayExpression',
elements: keys.map((file) => (
{ type: 'Literal', value: file, raw: JSON.stringify(file) }
)),
};
const contextMap = {
type: 'ObjectExpression',
properties: keys.map((key, i) => ({
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Literal', value: key, raw: JSON.stringify(key) },
value: {
type: 'ObjectExpression',
properties: [{
type: 'Property',
method: false,
shorthand: false,
computed: false,
key: { type: 'Identifier', name: 'default' },
value: { type: 'Identifier', name: modules[i] },
kind: 'init',
}],
},
kind: 'init',
})),
};
const contextFn = {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'context' },
init: {
type: 'ArrowFunctionExpression',
id: null,
expression: true,
generator: false,
async: false,
params: [{ type: 'Identifier', name: 'id' }],
body: {
type: 'MemberExpression',
object: contextMap,
property: { type: 'Identifier', name: 'id' },
computed: true,
optional: false,
},
},
},
],
kind: 'let',
};
const keyFn = {
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'context' },
property: { type: 'Identifier', name: 'keys' },
computed: false,
optional: false,
},
right: {
type: 'ArrowFunctionExpression',
id: null,
expression: true,
generator: false,
async: false,
params: [],
body: contextKeys,
},
},
};
const contextExpr = {
type: 'CallExpression',
callee: {
type: 'ArrowFunctionExpression',
id: null,
expression: false,
generator: false,
async: false,
params: [],
body: {
type: 'BlockStatement',
body: [
contextFn,
keyFn,
{
type: 'ReturnStatement',
argument: {
type: 'Identifier',
name: 'context',
},
},
],
},
},
arguments: [],
optional: false,
};
delete node.callee;
delete node.arguments;
delete node.optional;
Object.assign(node, contextExpr);
}));
ast.body.unshift(...imports);
dirWatchers[filePath] = dirs;
return astring.generate(ast);
}
module.exports = function (snowpackConfig, pluginOptions) {
return {
name: 'require-context-plugin',
resolve: {
input: Array.from(pluginOptions.input || ['.js']),
output: ['.js'],
},
onChange({ filePath }) {
for (const [source, dirs] of Object.entries(dirWatchers)) {
if (dirs.some((dir) => filePath.startsWith(dir))) {
this.markChanged(source);
}
}
},
load({ filePath }) {
return requireContext(filePath);
},
};
};
@@ -0,0 +1,18 @@
/* eslint-disable import/no-extraneous-dependencies */
import _ from 'lodash';
export default function mapWebpackUrlPrefix(mapList) {
const rules = mapList.map((mappingRule) => {
const regex = mappingRule.prefix.split('/').map((s) => _.escapeRegExp(s)).join('[\\/\\\\]');
return {
from: new RegExp(`^${regex}`, mappingRule.flag),
to: mappingRule.replace,
};
});
return function mapUrl(url) {
rules.forEach((rule) => {
url = url.replace(rule.from, rule.to);
});
return url;
};
}
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable */
import path from 'path';
export default function root(fn = '.') {
return path.resolve(__dirname, '../../', fn);
}
+8
View File
@@ -0,0 +1,8 @@
const { writeFileSync } = require('fs');
const { argv } = require('yargs');
const pkg = require('../package.json');
if (argv.dev) pkg.version = `${pkg.version}-dev`;
else pkg.version = pkg.version.replace('-dev', '');
writeFileSync(`${process.cwd()}/package.json`, JSON.stringify(pkg, null, 2));
+17
View File
@@ -0,0 +1,17 @@
aqua = #7FDBFF
blue = #0074D9
navy = #001F3F
teal = #39CCCC
green = #2ECC40
olive = #3D9970
lime = #01FF70
yellow = #FFDC00
orange = #FF851B
red = #FF4136
fuchsia = #F012BE
purple = #B10DC9
maroon = #85144B
white = #ffffff
silver = #dddddd
gray = #aaaaaa
black = #111111
@@ -0,0 +1,12 @@
// This file is imported in all stylus source code
breakpoints = json('../breakpoints.json', { hash: true })
rupture.mobile-cutoff = unit(breakpoints.mobile, 'px')
rupture.desktop-cutoff = unit(breakpoints.desktop, 'px')
rupture.hd-cutoff = unit(breakpoints.hd, 'px')
@import '~vj/misc/.iconfont/webicon.inc.styl'
@import 'color.inc.styl'
@import 'variables.inc.styl'
@import 'easing.inc.styl'
@import 'rem.inc.styl'
@import 'functions.inc.styl'
@@ -0,0 +1,39 @@
// Cubic
ease-in-cubic = cubic-bezier(0.550, 0.055, 0.675, 0.190);
ease-out-cubic = cubic-bezier(0.215, 0.610, 0.355, 1.000);
ease-in-out-cubic = cubic-bezier(0.645, 0.045, 0.355, 1.000);
// Circ
ease-in-circ = cubic-bezier(0.600, 0.040, 0.980, 0.335);
ease-out-circ = cubic-bezier(0.075, 0.820, 0.165, 1.000);
ease-in-out-circ = cubic-bezier(0.785, 0.135, 0.150, 0.860);
// Expo
ease-in-expo = cubic-bezier(0.950, 0.050, 0.795, 0.035);
ease-out-expo = cubic-bezier(0.190, 1.000, 0.220, 1.000);
ease-in-out-expo = cubic-bezier(1.000, 0.000, 0.000, 1.000);
// Quad
ease-in-quad = cubic-bezier(0.550, 0.085, 0.680, 0.530);
ease-out-quad = cubic-bezier(0.250, 0.460, 0.450, 0.940);
ease-in-out-quad = cubic-bezier(0.455, 0.030, 0.515, 0.955);
// Quart
ease-in-quart = cubic-bezier(0.895, 0.030, 0.685, 0.220);
ease-out-quart = cubic-bezier(0.165, 0.840, 0.440, 1.000);
ease-in-out-quart = cubic-bezier(0.770, 0.000, 0.175, 1.000);
// Quint
ease-in-quint = cubic-bezier(0.755, 0.050, 0.855, 0.060);
ease-out-quint = cubic-bezier(0.230, 1.000, 0.320, 1.000);
ease-in-out-quint = cubic-bezier(0.860, 0.000, 0.070, 1.000);
// Sine
ease-in-sine = cubic-bezier(0.470, 0.000, 0.745, 0.715);
ease-out-sine = cubic-bezier(0.390, 0.575, 0.565, 1.000);
ease-in-out-sine = cubic-bezier(0.445, 0.050, 0.550, 0.950);
// Back
ease-in-back = cubic-bezier(0.600, -0.280, 0.735, 0.045);
ease-out-back = cubic-bezier(0.175, 0.885, 0.320, 1.275);
ease-in-out-back = cubic-bezier(0.680, -0.550, 0.265, 1.550);
@@ -0,0 +1,9 @@
word-wrap()
overflow-wrap: break-word
word-wrap: break-word
-ms-word-break: break-all
word-break: break-word
-ms-hyphens: auto
-moz-hyphens: auto
-webkit-hyphens: auto
hyphens: auto
+7
View File
@@ -0,0 +1,7 @@
rem() {
ret = ();
for arg in arguments {
push(ret, unit(arg / $font-size, 'rem'));
}
return ret;
}
@@ -0,0 +1,257 @@
// layout
$grid-padding = 15px // half gutter
// fonts
$primary-font-family = "Open Sans", "Seravek", "Segoe UI", "Verdana", "PingFang SC", "Hiragino Sans GB", "Lantinghei SC", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans"
$header-font-family = "Open Sans", "Seravek", "Segoe UI", "Verdana", "PingFang SC", "Lantinghei SC", "Microsoft Yahei", "Hiragino Sans GB", "Microsoft Sans Serif", "WenQuanYi Micro Hei", "sans-serif"
$code-font-family = "monaco", "Source Code Pro", "Consolas", "Lucida Console", "monospace"
// primary palette
$default-color = #ddd
$primary-color = #5f9fd6
$secondary-color = #ed5f82
$highlight-color = #df6589
$code-color = #c92a2a
$text-1-color = #555
$text-2-color = lighten($text-1-color, 15%)
$text-3-color = lighten($text-1-color, 30%)
$header-1-color = #3d3d3d
$header-2-color = lighten($header-1-color, 20%)
$blockquote-color = #999
$page-bg-color = #edf0f2
$content-bg-color = #fff
$border-1-color = $default-color
$border-2-color = lighten($default-color, 20%)
$immersive-primary-color = #ffef87
$immersive-text-color = #f8f8f8
$immersive-header-color = #ffffff
$success-color = #25ad40
$fail-color = #fb5555
$progress-color = #f39800
$invalid-color = #9fa0a0
// sizes
$font-size = 16px // main content
$font-size-secondary = 14px // secondary content
$font-size-small = 13px // additional informations, usually gray colored
$font-size-icon = 16px
$font-size-title = 20px
// components
$section-gap-v = 25px
$section-gap-h = $grid-padding
$section-margin = 30px
$section-shadow = rem(0 6px 22px) rgba(#AFC2C9, 0.5)
$section-bg-color = $content-bg-color
$section-title-color = $header-2-color
$section-toolbtn-color = #AAA
$section-toolbtn-color-hover = #444
$autocomplete-empty-row-color = #AAA
$userselect-uid-color = #BBB
$editor-border-color = $border-1-color
$datepicker-shadow = rem(0 2px 7px) rgba(#000, 0.3)
$datapicker-list-item-bg-color-hover = #F0F0F0
$dialog-layer-bg-color = rgba(#F0F0F0, 0.6)
$dialog-bg-color = $content-bg-color
$dialog-border-color = $border-1-color
$dialog-shadow = rem(0 2px 10px) rgba(#000, 0.2)
$dialog-title-color = $header-2-color
$comment-placeholder-color = #AAA
$comment-op-color = lighten($text-1-color, 30%)
$comment-op-link-color = lighten($text-1-color, 50%)
$menu-item-bg-color-hover = #F4F4F4
$menu-drop-shadow = rem(0 2px 7px) rgba(#000, 0.3)
$menu-drop-bg-color = $content-bg-color
$menu-drop-triangle-size = 4px
$menu-drop-triangle-shadow = rem(-2px -2px 2px) rgba(#000, 0.15)
$nav-item-height = 45px
$nav-item-hover-color = $primary-color
$nav-item-active-color = $secondary-color
$nav-item-active-immersive-color = #9AE3F3
$nav-item-round-height = 32px
$nav-item-round-color = $primary-color
$nav-item-round-border = 2px
$nav-logo-width = 45px
$nav-logo-height = 23px
$header-layer-height = 100px
$header-bg-width = 1920px
$header-bg-height = 320px
$header-bg-height-mini = $header-bg-height - $header-layer-height
//$header-logo-width = 280px
//$header-logo-height = 150px
//$header-logo-width-real = 280px
//$header-logo-height-real = 150px
$header-logo-width = 252px
$header-logo-height = 150px
$header-logo-width-real = 381px
$header-logo-height-real = 201px
$header-logo-domain-lh = 60px
$header-logo-system-lh = 30px
$footer-bg-color = $content-bg-color
$input-focus-color = #C1E0FF
$form-control-height = 35px
$compact-control-height = 28px
$input-margin = 0 0 1rem
$input-multi-line-margin = 0 0.3rem 0.15rem 0
$input-image-radio-margin = 0 0.3rem 0.3rem 0
$input-background-color = $content-bg-color
$input-border = 1px solid $border-1-color
$input-outline = 2px solid transparent
$input-focus-border-color = $primary-color
$input-focus-outline = 2px solid $input-focus-color
$label-color = lighten($text-1-color, 10%)
$help-text-color = lighten($text-1-color, 30%)
$table-border-color = $border-2-color
$table-alternate-bg-color = #F4F4F4
$table-header-bg-color = rgba(#FFF, 0.95)
$table-header-shadow = rem(0 3px 2px) rgba(#000, 0.03)
$table-header-shadow-floating = rem(0 2px 5px) rgba(#000, 0.2)
$table-padding-h = 10px
$table-lh = 26px
$toolbar-bg = #f2f6f7
$toolbar-fore = #8395a1
$toolbar-fore-hover = #5a6b75
$toolbar-bg-hover = #e3e7e8
$toolbar-fore-selected = #FFF
$toolbar-bg-selected = $secondary-color
$toolbar-disabled-opacity = 0.3
$supplementary-color = lighten($text-1-color, 50%)
$supplementary-border-color = lighten($supplementary-color, 62%)
$supplementary-link-color = lighten($text-1-color, 40%)
$pager-border-color = $table-border-color
$nprogress-color = #FFF
$nprogress-color-floating = $primary-color
$syntax-hl-bg-color = #F8F8F8
// Calendar
$calendar-banner-color = {
'0': #FA6582,
'1': #EEA9C5,
'2': #FDB24D,
'3': #98CA86,
'4': #7DCBBF,
'5': #20BDE8,
'6': #6BCDDB,
'7': #6598FF,
'8': #8EB0F5,
'9': #928FE3,
'10': #676E7E,
'11': #A1A5AB,
}
// Notification
$notification-color = {
'success': #9BDB7B,
'info': #78D6F4,
'warn': #FCD279,
'error': #FD848D,
}
$notification-icon = {
'success': $icon-check--circle,
'info': $icon-info--circle,
'warn': $icon-info--circle,
'error': $icon-close--circle,
}
// Badge
$badge-text-color = {
'mod': #FFF,
'lv0': #727171,
'lv1': #FFF,
'lv2': #FFF,
'lv3': #FFF,
'lv4': #FFF,
'lv5': #FFF,
'lv6': #FFF,
'lv7': #FFF,
'lv8': #FFF,
'lv9': #FFF,
'lv10': #FFF,
}
$badge-bg-color = {
'mod': #cf70ab,
'lv0': #efefef,
'lv1': #9db4bd,
'lv2': #9db4bd,
'lv3': #73c3bc,
'lv4': #73c3bc,
'lv5': #81ba6c,
'lv6': #81ba6c,
'lv7': #f7b200,
'lv8': #ff9251,
'lv9': #eb6363,
'lv10': #575757,
}
// Record Status
$record-status-color = {
'pass': $success-color,
'fail': $fail-color,
'progress': $progress-color,
'ignored': $invalid-color,
'pending': $invalid-color,
}
$record-status-icon = {
'pass': $icon-check,
'fail': $icon-close,
'progress': $icon-hourglass,
'ignored': $icon-close,
'pending': $icon-schedule,
}
// Training Status
$training-status-color = {
'outside': $supplementary-color,
'progress': #e64c72,
'done': #25ad40,
}
$training-status-icon = {
'outside': $icon-help2,
'progress': $icon-schedule,
'done': $icon-check,
}
// Training Section Status
$training-section-status-color = {
'done': #25ad40,
'open': #e64c72,
'progress': #e64c72,
'invalid': $supplementary-color,
}
$training-section-status-icon = {
'done': $icon-check,
'open': $icon-help2,
'progress': $icon-schedule,
'invalid': $icon-close,
}
// Contest type
// #6EA2C7, #48AAB7, #6BB67A, #F5C735, #DC7D86
$contest-color = {
'acm': #6BB67A,
'oi': #F5C735,
'ioi': #2E9AFE,
'homework': #FFCDCD,
}
@@ -0,0 +1,125 @@
let removalChecker = null;
const res = [];
function checkResources() {
for (let i = res.length - 1; i >= 0; --i) {
if (res[i].detached || !document.body.contains(res[i].$dom[0])) {
if (!res[i].detached) res[i].detach();
res.splice(i, 1);
}
}
if (res.length === 0) {
clearInterval(removalChecker);
removalChecker = null;
}
}
function monitorResource(resource) {
res.push(resource);
if (removalChecker === null) {
removalChecker = setInterval(checkResources, 500);
}
}
export default class DOMAttachedObject {
static uniqueIdCounter = 0;
static get($obj) {
const key = this.DOMAttachKey;
return $obj.data(key);
}
static getOrConstruct($obj, ...args) {
const $singleObj = $obj.eq(0);
const key = this.DOMAttachKey;
const Protoclass = this;
const instance = this.get($singleObj);
if (instance !== undefined) return instance;
const newInstance = new Protoclass($singleObj, ...args);
// $dom may be updated in constructor so that we use $dom instead
// of $singleObj here.
if (!newInstance.$dom) return null;
newInstance.$dom.data(key, newInstance);
return newInstance;
}
static attachAll(container = document.body, ...args) {
if (process.env.NODE_ENV !== 'production') {
if (!this.DOMAttachSelector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMAttachSelector' should be specified`);
}
}
if (process.env.NODE_ENV !== 'production') {
console.time(`DOMAttachedObject[${this.DOMAttachKey}]: attachAll`);
}
$(container)
.find(this.DOMAttachSelector)
.addBack(this.DOMAttachSelector)
.get()
.forEach((element) => this.getOrConstruct($(element), ...args));
if (process.env.NODE_ENV !== 'production') {
console.timeEnd(`DOMAttachedObject[${this.DOMAttachKey}]: attachAll`);
}
}
static detachAll(container = document.body) {
const selector = this.DOMDetachSelector || this.DOMAttachSelector;
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMDetachSelector' or 'DOMAttachSelector' should be specified`);
}
}
if (process.env.NODE_ENV !== 'production') {
console.time(`DOMAttachedObject[${this.DOMAttachKey}]: detachAll`);
}
$(container)
.find(selector)
.addBack(selector)
.get()
.forEach((element) => {
const instance = this.get($(element));
if (instance) {
instance.detach();
}
});
if (process.env.NODE_ENV !== 'production') {
console.timeEnd(`DOMAttachedObject[${this.DOMAttachKey}]: detachAll`);
}
}
static registerLifeCycleHooks(attach = true) {
if (process.env.NODE_ENV !== 'production') {
if (!this.DOMAttachSelector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMAttachSelector' should be specified`);
}
}
$(document).on('vjContentNew', (e) => this.attachAll(e.target));
$(document).on('vjContentRemove', (e) => this.detachAll(e.target));
if (attach) {
this.attachAll();
}
}
detach() {
if (this.constructor.DOMAttachKey) {
this.$dom.removeData(this.constructor.DOMAttachKey);
}
this.detached = true;
}
constructor($dom, monitorDetach = false) {
if ($dom == null) {
return null;
}
this.$dom = $dom;
this.id = ++DOMAttachedObject.uniqueIdCounter;
this.eventNS = `vj4obj_${this.id}`;
this.detached = false;
if (monitorDetach) {
monitorResource(this);
}
}
}
@@ -0,0 +1,44 @@
import _ from 'lodash';
import tpl from 'vj/utils/tpl';
import request from 'vj/utils/request';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
function getText(domain) {
return domain._id;
}
function getItems(val) {
return request.get('/domain/search', { q: val });
}
function renderItem(domain) {
return tpl`
<div class="media">
<div class="media__left medium">
<img class="small domain-profile-avatar" src="${domain.gravatar}" width="30" height="30">
</div>
<div class="media__body medium">
<div class="domain-select__name">${domain.name}</div>
<div class="domain-select__id">ID = ${domain._id}</div>
</div>
</div>
`;
}
export default class DomainSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'vjDomainSelectAutoCompleteInstance';
constructor($dom, options) {
super($dom, {
classes: 'domain-select',
items: getItems,
render: renderItem,
text: getText,
...options,
});
}
}
_.assign(DomainSelectAutoComplete, DOMAttachedObject);
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
@@ -0,0 +1,44 @@
import _ from 'lodash';
import tpl from 'vj/utils/tpl';
import request from 'vj/utils/request';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
function getText(user) {
return user.uname;
}
function getItems(val) {
return request.get('/user/search', { q: val });
}
function renderItem(user) {
return tpl`
<div class="media">
<div class="media__left medium">
<img class="small user-profile-avatar" src="${user.gravatar}" width="30" height="30">
</div>
<div class="media__body medium">
<div class="user-select__uname">${user.uname}</div>
<div class="user-select__uid">UID = ${user._id}</div>
</div>
</div>
`;
}
export default class UserSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'vjUserSelectAutoCompleteInstance';
constructor($dom, options) {
super($dom, {
classes: 'user-select',
items: getItems,
render: renderItem,
text: getText,
...options,
});
}
}
_.assign(UserSelectAutoComplete, DOMAttachedObject);
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
@@ -0,0 +1,11 @@
.drop-element.autocomplete
max-width: rem(250px)
.empty-row
color: $autocomplete-empty-row-color
font-size: $(font-size-small)
padding: rem(10px)
.menu
max-height: rem(250px)
overflow: auto
@@ -0,0 +1,6 @@
.menu.domain-select
width: rem(200px)
.domain-select__id
font-size: rem($font-size-small)
color: $userselect-uid-color
@@ -0,0 +1,203 @@
import Drop from 'tether-drop';
import _ from 'lodash';
import 'jquery-scroll-lock';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import zIndexManager from 'vj/utils/zIndexManager';
export default class AutoComplete extends DOMAttachedObject {
static DOMAttachKey = 'vjAutoCompleteInstance';
constructor($dom, options = {}) {
super($dom);
this.options = {
items: async () => [],
render: () => '',
text: () => null,
minChar: 1,
cache: true,
clearDefaultValue: true,
position: 'bottom left',
classes: '',
...options,
};
this.clear(this.options.clearDefaultValue);
this.menuShown = false;
this.cache = {};
this.currentItems = [];
this.$menu = $(`<ol class="menu ${this.options.classes}"></ol>`);
this.$menu.scrollLock({ strict: false });
this.$menu.on('mousedown', this.onMenuClick.bind(this));
this.$menu.on('mousedown', '.menu__item', this.onItemClick.bind(this));
this.dropInstance = new Drop({
classes: 'autocomplete dropdown',
target: this.$dom[0],
content: this.$menu[0],
position: this.options.position,
constrainToWindow: false,
constrainToScrollParent: false,
openOn: false,
});
this.dropInstance.on('open', this.onDropOpen.bind(this));
this.attach();
}
clear(clearValue = true) {
if (clearValue) {
this.$dom.val('');
}
this._value = null;
this.lastText = null;
}
attach() {
this.$dom.on(`click.${this.eventNS}`, this.onClick.bind(this));
this.$dom.on(`focus.${this.eventNS}`, this.onFocus.bind(this));
this.$dom.on(`blur.${this.eventNS}`, this.onBlur.bind(this));
this.$dom.on(`keydown.${this.eventNS}`, this.onKeyDown.bind(this));
this.$dom.on(`keyup.${this.eventNS}`, this.onKeyUp.bind(this));
}
onClick() {
this.updateOpenState();
}
onFocus() {
this.isFocus = true;
this.updateOpenState();
}
onBlur() {
this.isFocus = false;
this.updateOpenState();
}
onKeyDown() {
// TODO: Implement keyboard navigation
}
onKeyUp(ev) {
if (ev.which === 27) {
// ESC
this.close();
return;
}
if (this.$dom.val() === this.lastText) {
return;
}
this.lastText = this.$dom.val();
this.updateOpenState();
if (this.isOpen) {
this.renderList();
}
}
onMenuClick(ev) {
ev.preventDefault(); // prevent from losing focus
}
onItemClick(ev) {
const idx = $(ev.currentTarget).attr('data-idx');
if (idx === undefined) {
return;
}
const item = this.currentItems[idx];
const text = this.options.text(item);
this._value = item;
this.$dom.trigger('vjAutoCompleteSelect', item);
if (text != null) {
this.$dom.val(text);
}
this.close();
}
onDropOpen() {
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
}
async getItems(val) {
if (this.cache[val] !== undefined) {
return this.cache[val];
}
const data = await this.options.items(val);
if (this.options.cache) {
this.cache[val] = data;
}
return data;
}
getHtml(items) {
if (items.length === 0) {
return tpl`<div class="empty-row">${i18n('Oops, there are no results.')}</div>`;
}
return items.map((item, idx) => `
<li class="menu__item" data-idx="${idx}"><a href="javascript:;" class="menu__link">
${this.options.render(item)}
</a></li>
`).join('\n');
}
async renderList() {
const val = this.$dom.val();
const items = await this.getItems(val);
const html = this.getHtml(items);
this.currentItems = items;
this.$menu.html(html);
}
open() {
if (this.isOpen) {
return;
}
this.dropInstance.open();
this.isOpen = true;
}
close() {
if (!this.isOpen) {
return;
}
this.dropInstance.close();
this.isOpen = false;
}
updateOpenState() {
if (this.isFocus && this.$dom.val().length >= this.options.minChar) {
if (!this.isOpen) {
this.open();
this.renderList();
}
} else {
if (this.isOpen) {
this.close();
}
}
}
detach() {
if (this.detached) {
return;
}
super.detach();
this.$dom.off(`focus.${this.eventNS}`);
this.$dom.off(`blur.${this.eventNS}`);
this.$dom.off(`keydown.${this.eventNS}`);
this.$dom.off(`keyup.${this.eventNS}`);
this.dropInstance.destroy();
this.$menu.remove();
}
value() {
return this._value;
}
focus() {
this.$dom.focus();
}
}
_.assign(AutoComplete, DOMAttachedObject);
window.Hydro.components.autocomplete = AutoComplete;
@@ -0,0 +1,6 @@
.menu.user-select
width: rem(200px)
.user-select__uid
font-size: rem($font-size-small)
color: $userselect-uid-color
@@ -0,0 +1,129 @@
.calendar__header
line-height: rem(45px)
text-align: center
h1
display: inline-block
line-height: inherit
width: rem(280px)
button
font-size: rem($font-size-title)
color: $text-3-color
padding: 0 rem(10px)
outline: 0
&:hover
color: $primary-color
.calendar__week-title
table
width: 100%
th
width: 14.28%
padding: rem(20px 10px)
color: $supplementary-color
.calendar__row
position: relative
.calendar__row__bg
position: absolute
left: 0
top: 0
width: 100%
height: 100%
table
width: 100%
height: 100%
td
width: 14.28%
border: 1px solid $table-border-color
&.is-current-day
background: lighten($highlight-color, 85%)
.calendar__row__content
min-height: rem(100px)
position: relative
z-index: 2
padding-bottom: rem(10px)
table
width: 100%
th
width: 14.28%
padding: rem(10px)
color: $text-2-color
&.is-inactive
opacity: 0.3
&.is-current-day
color: $highlight-color
td
position: relative
height: rem(24px)
.calendar__banner
position: absolute
display: block
top: rem(2px)
left: rem(5px)
right: rem(5px)
height: rem(20px)
line-height: rem(20px)
padding: rem(0 5px)
font-size: rem($font-size-small)
border-radius: rem(5px)
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
&, &:visited, &:hover, &:active
color: #FFF
text-decoration: none
&.is-trunc-begin, &.is-snap-begin
border-top-left-radius: 0
border-bottom-left-radius: 0
&.is-trunc-end, &.is-snap-end
border-top-right-radius: 0
border-bottom-right-radius: 0
&.is-snap-begin
left: 0
padding-left: rem(10px)
&.is-snap-end
right: 0
padding-right: rem(10px)
&.is-masked
background-image: repeating-linear-gradient(
45deg,
rgba(#000, 0.1),
rgba(#000, 0.1) 10px,
rgba(#000, 0) 10px,
rgba(#000, 0) 20px
);
for key, value in $calendar-banner-color
&.color-{key}
background-color: value
.calendar__body-container
position: relative
.calendar__body.exit
position: absolute
left: 0
top: 0
width: 100%
user-select: none
@@ -0,0 +1,367 @@
import _ from 'lodash';
import moment from 'moment';
import tpl from 'vj/utils/tpl';
export default class Calendar {
constructor(events) {
this.$dom = $(tpl`
<div class="calendar">
<div class="calendar__header">
<button name="prev"><span class="icon icon-chevron_left"></span></button>
<h1 class="calendar__header__title"></h1>
<button name="next"><span class="icon icon-chevron_right"></span></button>
</div>
<div class="calendar__week-title">
<table>
<thead><tr>
<th>SUN</th>
<th>MON</th>
<th>TUE</th>
<th>WED</th>
<th>THU</th>
<th>FRI</th>
<th>SAT</th>
</tr></thead>
</table>
</div>
<div class="calendar__body-container"></div>
</div>
`);
this.events = events.map((ev) => ({
...ev,
beginAt: moment(ev.beginAt),
endAt: moment(ev.endAt),
maskFrom: ev.maskFrom ? moment(ev.maskFrom) : null,
}));
this.$dom.find('[name="prev"]').click(() => this.navToPrev());
this.$dom.find('[name="next"]').click(() => this.navToNext());
this.$lastBody = null;
this.navToToday();
}
getDom() {
return this.$dom;
}
navToToday() {
if (this.animating) {
return;
}
this.current = moment().date(1);
this.update();
}
navToNext() {
if (this.animating) {
return;
}
this.current.add('months', 1);
this.update(1);
}
navToPrev() {
if (this.animating) {
return;
}
this.current.subtract('months', 1);
this.update(-1);
}
update(direction = 0) {
this.updateHeader();
this.updateBody(direction);
}
updateHeader() {
this.$dom.find('.calendar__header__title').text(this.current.format('MMMM YYYY'));
}
async updateBody(direction) {
this.animating = true;
const $newBody = this.buildBody();
$newBody.appendTo(this.$dom.find('.calendar__body-container'));
if (this.$lastBody !== null) {
this.$lastBody
.addClass('exit')
.transition({
y: direction * 100,
opacity: 0,
}, {
duration: 500,
easing: 'easeOutCubic',
});
await $newBody
.css({
y: -direction * 100,
opacity: 0,
})
.transition({
y: 0,
opacity: 1,
}, {
duration: 500,
easing: 'easeOutCubic',
})
.promise();
this.$lastBody.remove();
}
this.$lastBody = $newBody;
this.animating = false;
}
buildBody() {
const data = this.buildBodyData();
const $body = $('<div class="calendar__body"></div>');
data.forEach((week) => {
const $row = $(tpl`<div class="calendar__row">
<div class="calendar__row__bg"><table><tbody><tr></tr></tbody></table></div>
<div class="calendar__row__content"><table><thead><tr></tr></thead><tbody></tbody></table></div>
</div>`);
const $bgContainer = $row.find('.calendar__row__bg tr');
const $numContainer = $row.find('.calendar__row__content thead tr');
const $bannerContainer = $row.find('.calendar__row__content tbody');
week.days.forEach((day) => {
const isInactive = day.active ? '' : 'is-inactive';
const isCurrentDay = day.current ? 'is-current-day' : '';
const today = day.current ? ' (TODAY)' : '';
$bgContainer.append($('<td></td>').addClass(isInactive).addClass(isCurrentDay));
$numContainer.append($(tpl`<th>${day.date.format('D')}${today}</th>`).addClass(isInactive).addClass(isCurrentDay));
});
week.banners.forEach((banners) => {
const $tr = $('<tr/>');
banners.forEach((bannerSpan) => {
if (!bannerSpan.banner) {
$tr.append(tpl`<td colspan="${bannerSpan.span}"></td>`);
return;
}
const $cell = $(tpl`<td colspan="${bannerSpan.span}"></td>`);
const $banner = $(tpl`
<a
href="${bannerSpan.banner.event.link}"
class="calendar__banner color-${bannerSpan.banner.event.colorIndex}"
>${bannerSpan.banner.mask ? bannerSpan.banner.event.maskTitle : bannerSpan.banner.event.title}</a>
`);
if (bannerSpan.banner.mask) {
$banner.addClass('is-masked');
}
if (bannerSpan.banner.beginTrunc) {
$banner.addClass('is-trunc-begin');
} else if (bannerSpan.banner.beginSnap) {
$banner.addClass('is-snap-begin');
}
if (bannerSpan.banner.endTrunc) {
$banner.addClass('is-trunc-end');
} else if (bannerSpan.banner.endSnap) {
$banner.addClass('is-snap-end');
}
$cell.append($banner);
$tr.append($cell);
});
$bannerContainer.append($tr);
});
$body.append($row);
});
return $body;
}
buildBodyData() {
const now = moment();
const days = [];
{
// back fill
const base = this.current.clone();
const dayOfWeek = base.day();
if (dayOfWeek > 0) {
base.subtract(dayOfWeek + 1, 'days');
for (let i = dayOfWeek; i > 0; --i) {
days.push({
active: false,
date: base.add(1, 'days').clone(),
});
}
}
}
{
// current month
const base = this.current.clone();
while (base.month() === this.current.month()) {
days.push({
active: true,
date: base.clone(),
});
base.add(1, 'days');
}
}
{
// forward fill
const base = this.current.clone().add(1, 'months').subtract(1, 'days');
const dayOfWeek = base.day();
if (dayOfWeek < 6) {
for (let i = dayOfWeek; i < 6; ++i) {
days.push({
active: false,
date: base.add(1, 'days').clone(),
});
}
}
}
days.forEach((day) => {
day.current = day.date.isSame(now, 'day'); // eslint-disable-line no-param-reassign
});
const daysByWeek = _.chunk(days, 7);
const numberOfWeeks = days.length / 7;
const bannersByWeek = _.fill(new Array(numberOfWeeks), 1).map(() => []);
const beginDate = days[0].date.clone();
const endDate = _.last(days).date.clone();
// cut events by week to banners
this.events.forEach((ev) => {
if (ev.endAt.isBefore(ev.beginAt, 'day')) {
return;
}
if (ev.endAt.isBefore(beginDate, 'day') || ev.beginAt.isAfter(endDate, 'day')) {
return;
}
if (ev.beginAt.hour() >= 22) {
ev.beginAt.add(1, 'day').startOf('day');
}
if (ev.endAt.hour() <= 2) {
ev.endAt.subtract(1, 'day').endOf('day');
}
let [bannerBegin, bannerBeginTruncated] = [ev.beginAt.clone(), false];
if (bannerBegin.isBefore(beginDate, 'day')) {
[bannerBegin, bannerBeginTruncated] = [beginDate.clone(), true];
}
do {
const bannerEndMax = bannerBegin.clone().endOf('week');
let [bannerEnd, bannerEndTruncated] = [ev.endAt.clone(), false];
if (bannerEnd.isAfter(bannerEndMax, 'day')) {
[bannerEnd, bannerEndTruncated] = [bannerEndMax, true];
}
const weekIndex = bannerBegin.clone().startOf('day').diff(beginDate.clone().startOf('day'), 'week');
bannersByWeek[weekIndex].push({
beginAt: bannerBegin.startOf('day'),
beginTrunc: bannerBeginTruncated,
endAt: bannerEnd.endOf('day'),
endTrunc: bannerEndTruncated,
event: ev,
});
if (!bannerEndTruncated) {
break;
}
[bannerBegin, bannerBeginTruncated] = [bannerEnd.clone().add(1, 'day'), true];
} while (!bannerBegin.isAfter(endDate, 'day'));
});
// layout banners
const layout = bannersByWeek
.map((banners) => _
.sortBy(banners, [
(banner) => banner.beginAt.valueOf(),
(banner) => (banner.beginTrunc ? 0 : 1), // truncated events first
(banner) => -banner.endAt.valueOf(), // long events first
]))
.map((banners) => {
const dayBitmap = _
.fill(new Array(7), 1)
.map(() => []);
banners.forEach((banner) => {
const beginDay = banner.beginAt.day();
const endDay = banner.endAt.day();
// find available space
const vIndexMax = _.max(_
.range(beginDay, endDay + 1)
.map((day) => dayBitmap[day].length));
let vIndex = 0;
for (; vIndex < vIndexMax; ++vIndex) {
if (_.every(_
.range(beginDay, endDay + 1)
.map((day) => !dayBitmap[day][vIndex]) // eslint-disable-line no-loop-func
)) { // eslint-disable-line function-paren-newline
break;
}
}
// fill space
for (let i = beginDay; i <= endDay; ++i) {
dayBitmap[i][vIndex] = banner;
}
});
// merge adjacent cells and arrange banners by vertical index
const vMaxLength = _.max(_.range(0, 7).map((day) => dayBitmap[day].length));
const weekBanners = _
.fill(new Array(vMaxLength), 1)
.map(() => []);
for (let vIndex = 0; vIndex < vMaxLength; ++vIndex) {
let last = { span: 1, banner: dayBitmap[0][vIndex] };
weekBanners[vIndex].push(last);
for (let day = 1; day < 7; ++day) {
const banner = dayBitmap[day][vIndex];
if (banner !== last.banner) {
last = { span: 1, banner };
weekBanners[vIndex].push(last);
} else {
last.span++;
}
}
}
// cut banners by masked areas, scanning from left to right
weekBanners.forEach((bannerSpans) => {
for (let i = 0; i < bannerSpans.length; ++i) {
const { banner } = bannerSpans[i];
if (!banner) {
continue;
}
if (banner.mask) {
continue;
}
if (!banner.event.maskFrom) {
continue;
}
if (banner.event.maskFrom.isSame(banner.event.endAt)) {
// do not show masks if maskFrom === endAt
continue;
}
if (banner.event.endAt.isSame(banner.event.beginAt, 'day')) {
// do not show masks if endAt - beginAt <= 1 day
continue;
}
if (banner.event.maskFrom.isAfter(banner.endAt, 'day')) {
// we are not in the time region for masking
continue;
}
if (banner.event.maskFrom.isSameOrBefore(banner.beginAt, 'day')) {
// mask begins before this banner: replace current banner with masked banner
banner.mask = true;
} else {
// mask begins during this banner: cut current banner into two pieces
const newBanner = {
...banner,
beginAt: banner.event.maskFrom.clone(),
beginSnap: true,
beginTrunc: false,
mask: true,
};
const newBannerSpan = {
span: newBanner.endAt.day() - newBanner.beginAt.day() + 1,
banner: newBanner,
};
banner.endAt = banner.event.maskFrom.clone().subtract(1, 'day');
banner.endSnap = true;
bannerSpans[i].span -= newBannerSpan.span; // eslint-disable-line no-param-reassign
bannerSpans.splice(i + 1, 0, newBannerSpan);
i++;
}
}
});
return weekBanners;
});
return daysByWeek
.map((daysInWeek, weekIndex) => ({
days: daysInWeek,
banners: layout[weekIndex],
}));
}
}
@@ -0,0 +1,20 @@
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import CmEditor from '.';
function runSubstitute($container) {
const selector = ['textarea[data-markdown]'];
$container.find(selector.join(', ')).get().forEach((element) => {
CmEditor.getOrConstruct($(element));
});
}
const cmEditorPage = new AutoloadPage('cmEditorPage', () => {
runSubstitute($('body'));
$(document).on('vjContentNew', async (e) => {
await delay(0);
runSubstitute($(e.target));
});
});
export default cmEditorPage;
@@ -0,0 +1,91 @@
.simplemde
border: 1px solid $editor-border-color
margin: 0 0 1rem
position: relative
height: rem(200px)
box-sizing: border-box
overflow: hidden
.cm-s-simplemde
border: 0
min-height: 0
height: auto
padding: 0
position: absolute
left: 0
top: 40px
width: 100%
bottom: 0
color: inherit
.simplemde-toolbar
position: absolute
left: 0
top: 0
width: 100%
border-radius: 0
border: 0
opacity: 0
background: $toolbar-bg
transform: translateY(-20px)
transition: opacity .5s, transform .5s
transition-timing-function: ease-out-cubic
&:hover
opacity: 1
.toolbar--visible .simplemde-toolbar
opacity: 1
transform: translateY(0)
.cm-s-simplemde .CodeMirror-scroll
height: 100%
min-height: 0
font-family: $code-font-family
color: $text-1-color
.simplemde-toolbar:before,
.simplemde-toolbar:after
display: none
.simplemde-toolbar
padding: rem(5px)
.simplemde-toolbar i.separator
border-right: 0
.simplemde-toolbar a
color: $toolbar-fore !important
border: 0
background: none
border-radius: 0
font-size: rem($font-size-icon)
outline: 0
&:hover
background: $toolbar-bg-hover
color: $toolbar-fore-hover !important
border: 0
&.active
background: $toolbar-bg-selected
color: $toolbar-fore-selected !important
border: 0
.simplemde-toolbar.disabled-for-preview a:not(.no-disable)
background: none
opacity: $toolbar-disabled-opacity
.cm-s-simplemde .CodeMirror-code .cm-comment
background: none
.simplemde-statusbar
display: none
.simplemde-preview
background: #FFF
.no-heading .cm-s-simplemde .CodeMirror-code
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4
font-size: 1em
line-height: 1.5
@@ -0,0 +1,66 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
export default class CmEditor extends DOMAttachedObject {
static DOMAttachKey = 'vjCmEditorInstance';
constructor($dom, options = {}) {
super($dom);
this.editor = null;
this.options = options;
this.init();
}
async init() {
const { default: VjCmEditor } = await import('./vjcmeditor');
const hasFocus = this.$dom.is(':focus');
const srcHeight = this.$dom.outerHeight();
this.editor = new VjCmEditor({
...this.options,
element: this.$dom.get(0),
});
this.moveToEnd();
const $editor = $(this.editor.wrapper);
$editor.css('height', srcHeight);
$editor.addClass('toolbar--visible');
if (hasFocus) {
this.focus();
}
}
isValid() {
return (this.editor !== null);
}
ensureValid() {
if (!this.isValid()) {
throw new Error('VjCmEditor is not loaded');
}
}
value(...argv) {
this.ensureValid();
const ret = this.editor.value(...argv);
if (argv.length > 0) {
this.moveToEnd();
}
return ret;
}
focus() {
this.ensureValid();
this.editor.codemirror.focus();
}
moveToEnd() {
this.ensureValid();
const cm = this.editor.codemirror;
cm.setCursor(cm.lineCount(), 0);
}
}
_.assign(CmEditor, DOMAttachedObject);
@@ -0,0 +1,32 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import CmEditor from '.';
export default class TextareaHandler extends DOMAttachedObject {
static DOMAttachKey = 'vjTextareaHandlerInstance';
getCmEditor() {
return CmEditor.get(this.$dom);
}
isCmEditor() {
const editor = this.getCmEditor();
return (editor !== undefined && editor.isValid());
}
val(...argv) {
if (this.isCmEditor()) {
return this.getCmEditor().value(...argv);
}
return this.$dom.val(...argv);
}
focus() {
if (this.isCmEditor()) {
this.getCmEditor().focus();
}
this.$dom.focus();
}
}
_.assign(TextareaHandler, DOMAttachedObject);
@@ -0,0 +1,120 @@
import _ from 'lodash';
import SimpleMDE from 'vj-simplemde';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
export default class VjCmEditor extends SimpleMDE {
constructor(options = {}) {
super({
autoDownloadFontAwesome: false,
spellChecker: false,
forceSync: true,
toolbar: [
{
name: 'bold',
action: SimpleMDE.toggleBold,
className: 'icon icon-bold',
title: i18n('Bold'),
},
{
name: 'italic',
action: SimpleMDE.toggleItalic,
className: 'icon icon-italic',
title: i18n('Italic'),
},
'|',
{
name: 'quote',
action: SimpleMDE.toggleBlockquote,
className: 'icon icon-quote',
title: i18n('Quote'),
},
{
name: 'unordered-list',
action: SimpleMDE.toggleUnorderedList,
className: 'icon icon-unordered_list',
title: i18n('Unordered List'),
},
{
name: 'ordered-list',
action: SimpleMDE.toggleOrderedList,
className: 'icon icon-ordered_list',
title: i18n('Ordered List'),
},
'|',
{
name: 'code',
action: () => this.insertCodeBlock(),
className: 'icon icon-code',
title: i18n('Insert Code'),
default: true,
},
{
name: 'link',
action: SimpleMDE.drawLink,
className: 'icon icon-link',
title: i18n('Create Link'),
default: true,
},
{
name: 'image',
action: SimpleMDE.drawImage,
className: 'icon icon-insert--image',
title: i18n('Insert Image'),
default: true,
},
'|',
{
name: 'preview',
action: SimpleMDE.togglePreview,
preAction: SimpleMDE.preRenderPreview,
className: 'icon icon-preview no-disable',
title: i18n('Toggle Preview'),
default: true,
},
],
commonmark: {
safe: true,
},
...options,
});
}
async markdown(text) {
// FIXME preview wont update after first toggle
const data = await request.ajax({
url: '/markdown',
method: 'post',
data: $.param({ text }, true),
dataType: 'text',
});
_.defer(this.preparePreview.bind(this));
return data;
}
preparePreview() {
const $preview = $(this.wrapper).find('.simplemde-preview');
$preview.addClass('typo');
$preview.attr('data-emoji-enabled', 'true');
$preview.trigger('vjContentNew');
}
insertCodeBlock() {
const text = this.codemirror.getSelection();
const startPoint = this.codemirror.getCursor('start');
const endPoint = this.codemirror.getCursor('end');
const leadingLines = (startPoint.line === 0 && startPoint.ch === 0) ? 0 : 2;
// eslint-disable-next-line prefer-template
const startText = _.repeat('\n', leadingLines) + '```cpp\n';
const endText = '\n```\n';
this.codemirror.replaceSelection(`${startText}${text}${endText}`);
startPoint.line += leadingLines + 1;
startPoint.ch = 0;
if (startPoint !== endPoint) {
endPoint.line += leadingLines + 1;
}
this.codemirror.setSelection(startPoint, endPoint);
this.codemirror.focus();
}
}
@@ -0,0 +1,34 @@
.contest-type-tag
.contest-tag
font-size: rem($font-size-small)
display: inline-block
line-height: rem(22px)
padding: rem(0 10px)
vertical-align: middle
border-radius: rem(12px)
&:hover
text-decoration: none
color: #FFF !important
.contest-type-tag
&, &:visited
&, .supplementary &
color: #FFF
.icon
margin-right: rem(5px)
for key, value in $contest-color
.contest-type--{key} .contest-type-tag
background: value
&:hover
background: lighten(value, 20%)
.contest-tag
border: 1px solid $supplementary-border-color
&:hover
background: $primary-color
border-color: $primary-color
@@ -0,0 +1,21 @@
.contest-sidebar__bg
display: block
background: #67AABB url(problem-contest-bg.png) right top no-repeat
color: #FFF !important
font-size: rem($font-size)
padding: rem(20px 0)
+retina()
background-image: url(problem-contest-bg@2x.png)
background-size: 131px 150px
h1
font-size: rem($font-size-title)
color: #FFF
text-shadow: 0 1px 3px rgba(#000, 0.5)
&:hover
text-decoration: none
.contest-sidebar__status
margin-top: rem(20px)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1,21 @@
import { AutoloadPage } from 'vj/misc/Page';
const datepickerPage = new AutoloadPage('datepickerPage', async () => {
if ($('[data-pick-date]').length > 0) {
await import('pickadate/lib/picker.date');
$('[data-pick-date]').pickadate({
format: 'yyyy-m-d',
clear: false,
});
}
if ($('[data-pick-time]').length > 0) {
await import('pickadate/lib/picker.time');
$('[data-pick-time]').pickatime({
format: 'H:i',
interval: 15,
clear: false,
});
}
});
export default datepickerPage;
@@ -0,0 +1,54 @@
.picker__holder
margin-top: -1rem
border-radius: 0
transform: scale(0.9) translateZ(0)
transform-origin: 0 0
outline: 0
font-size: rem($font-size-secondary)
transition: transform .1s ease-in-out,
opacity .1s ease-in-out,
max-height 0s .1s,
border-width 0s .1s
.picker--opened .picker__holder
transform: scale(1) translateZ(0)
transition: transform .1s ease-in-out,
opacity .1s ease-in-out,
max-height 0s,
border-width 0s
box-shadow: $data-picker-shadow
.picker__list
padding: 0
.picker__list-item
border: 0
.picker__list-item:hover
background: $datapicker-list-item-bg-color-hover
.picker__list-item--selected, .picker__list-item--selected:hover, .picker--focused .picker__list-item--selected
background: $primary-color
.picker__day--infocus:hover, .picker__day--outfocus:hover
background: $datapicker-list-item-bg-color-hover
.picker__day--highlighted
border-color: transparent
.picker__day--highlighted:hover, .picker--focused .picker__day--highlighted
background: transparent
.picker__day--selected, .picker__day--selected:hover, .picker--focused .picker__day--selected
background: $primary-color
.picker__button--today:hover, .picker__button--clear:hover, .picker__button--close:hover
background: $datapicker-list-item-bg-color-hover
border-color: $datapicker-list-item-bg-color-hover
.picker__button--today:focus, .picker__button--clear:focus, .picker__button--close:focus
background: $datapicker-list-item-bg-color-hover
border-color: $datapicker-list-item-bg-color-hover
.picker__nav--prev:hover, .picker__nav--next:hover
background: $datapicker-list-item-bg-color-hover
@@ -0,0 +1,145 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import zIndexManager from 'vj/utils/zIndexManager';
export default class DomDialog extends DOMAttachedObject {
static DOMAttachKey = 'vjDomDialogInstance';
constructor($dom, options = {}) {
super($dom);
this.isShown = false;
this.isAnimating = false;
this.options = {
cancelByClickingBack: false,
cancelByEsc: false,
onDispatch: () => {},
...options,
};
this._defer = null;
}
async _show() {
this.$dom.css('z-index', zIndexManager.getNext());
this.$dom.trigger('vjDomDialogShow');
this.isAnimating = true;
if (this.options.cancelByClickingBack) {
this.$dom.on(`click.${this.eventNS}`, this.handleClick.bind(this));
}
if (this.options.cancelByEsc) {
$(document).on(`keyup.${this.eventNS}`, this.handleKeyUp.bind(this));
}
const $wrap = this.$dom;
$wrap.css({
display: 'flex',
opacity: 0,
});
$wrap.width();
$wrap.transition({
opacity: 1,
}, {
duration: 100,
easing: 'easeOutCubic',
});
const $dgContent = this.$dom.find('.dialog__content');
$dgContent.css({
scale: 0.8,
});
await $dgContent
.transition({
scale: 1,
}, {
duration: 200,
easing: 'easeOutCubic',
})
.promise();
this.$dom.find('[data-autofocus]').focus();
this.isShown = true;
this.isAnimating = false;
this.$dom.trigger('vjDomDialogShown');
}
async _hide() {
this.$dom.trigger('vjDomDialogHide');
this.isAnimating = true;
$(document).off(`keyup.${this.eventNS}`);
this.$dom.off(`click.${this.eventNS}`);
this.$dom.css({
opacity: 1,
});
this.$dom.transition({
opacity: 0,
}, {
duration: 200,
});
const $dgContent = this.$dom.find('.dialog__content');
$dgContent.css({
scale: 1,
});
await $dgContent
.transition({
scale: 0.8,
}, {
duration: 200,
easing: 'easeOutCubic',
})
.promise();
this.$dom.css('display', 'none');
this.isShown = false;
this.isAnimating = false;
this.$dom.trigger('vjDomDialogHidden');
}
show() {
if (this.isShown || this.isAnimating) {
return Promise.reject();
}
this._defer = new $.Deferred();
this._show();
return this._defer.promise();
}
hide() {
if (!this.isShown || this.isAnimating) {
return false;
}
if (this._defer.state() === 'pending') {
this._defer.resolve('cancel');
}
this._defer = null;
this._hide();
return true;
}
dispatchAction(data) {
if (this.options.onDispatch(data) === false) {
return;
}
this._defer.resolve(data);
this.hide();
}
handleClick(e) {
if (e.target === this.$dom.get(0)) {
this.dispatchAction('cancel');
}
}
handleKeyUp(e) {
if (e.keyCode === 27) {
this.dispatchAction('cancel');
}
}
}
_.assign(DomDialog, DOMAttachedObject);
@@ -0,0 +1,38 @@
.dialog
position: fixed
left: 0
top: 0
width: 100%
height: 100%
opacity: 0
z-index: 1000 // managed
// display flex will be added after animation completes
flex-direction: column
align-items: center
justify-content: center
display: none
background: $dialog-layer-bg-color
.dialog__content
position: relative
.dialog.withBg .dialog__content
background: $dialog-bg-color
border: 1px solid $dialog-border-color
box-shadow: $dialog-shadow
min-width: rem(500px)
max-width: rem(900px)
padding: rem(40px) rem(30px)
.dialog__body
margin-bottom: rem(20px)
min-height: rem(50px)
h1
font-size: 1.6rem
color: $dialog-title-color
margin-bottom: 1rem
.dialog__action button
margin-left: rem(10px)
margin-bottom: 0
@@ -0,0 +1,99 @@
import tpl from 'vj/utils/tpl';
import i18n from 'vj/utils/i18n';
import DomDialog from './DomDialog';
export default class Dialog {
constructor(options = {}) {
this.options = {
classes: '',
$body: null,
$action: null,
...options,
};
this.$dom = $(tpl`
<div class="dialog withBg ${this.options.classes}" style="display:none">
<div class="dialog__content">
<div class="dialog__body"></div>
<div class="row"><div class="columns clearfix">
<div class="float-right dialog__action"></div>
</div></div>
</div>
</div>
`);
this.$dom.on('click', '[data-action]', this.handleActionButton.bind(this));
this.$dom.on('vjDomDialogShow', this.beforeShow.bind(this));
this.$dom.on('vjDomDialogHidden', this.afterHide.bind(this));
this.$dom.find('.dialog__body').append(this.options.$body);
this.$dom.find('.dialog__action').append(this.options.$action);
this.domDialogInstance = new DomDialog(this.$dom, this.options);
}
beforeShow() {
this.$dom.appendTo('body');
}
afterHide() {
this.$dom.detach();
}
open() {
return this.domDialogInstance.show();
}
close() {
return this.domDialogInstance.hide();
}
handleActionButton(ev) {
this.domDialogInstance.dispatchAction($(ev.currentTarget).attr('data-action'));
}
}
const buttonOk = tpl`<button class="primary rounded button" data-action="ok">${i18n('Ok')}</button>`;
const buttonCancel = tpl`<button class="rounded button" data-action="cancel">${i18n('Cancel')}</button>`;
const buttonYes = tpl`<button class="primary rounded button" data-action="yes">${i18n('Yes')}</button>`;
const buttonNo = tpl`<button class="rounded button" data-action="no">${i18n('No')}</button>`;
export class InfoDialog extends Dialog {
constructor(options = {}) {
super({
$action: buttonOk,
cancelByClickingBack: true,
cancelByEsc: true,
...options,
});
}
}
export class ActionDialog extends Dialog {
constructor(options = {}) {
super({
$action: [buttonCancel, buttonOk].join('\n'),
cancelByClickingBack: true,
cancelByEsc: true,
...options,
});
}
}
export class ConfirmDialog extends Dialog {
constructor(options = {}) {
let buttons = [];
if (options.canCancel) {
buttons = [buttonCancel, buttonNo, buttonYes];
} else {
buttons = [buttonNo, buttonYes];
}
super({
$action: buttons.join('\n'),
cancelByClickingBack: options.canCancel,
cancelByEsc: options.canCancel,
...options,
});
}
}
window.Hydro.components.Dialog = Dialog;
window.Hydro.components.InfoDialog = InfoDialog;
window.Hydro.components.ActionDialog = ActionDialog;
window.Hydro.components.ConfirmDialog = ConfirmDialog;
@@ -0,0 +1,105 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import request from 'vj/utils/request';
import TextareaHandler from 'vj/components/cmeditor/textareaHandler';
import Notification from 'vj/components/notification';
let initialized = false;
const $template = $('.dczcomments__box').eq(0).clone();
function getClosestInstance($dom) {
return $dom.closest('.dczcomments__box').data('instance');
}
function onBoxCancel(ev) {
const commentBox = getClosestInstance($(ev.currentTarget));
if (commentBox) commentBox.onCancel(ev);
}
function onBoxSubmit(ev) {
const commentBox = getClosestInstance($(ev.currentTarget));
if (commentBox) commentBox.onSubmit(ev);
ev.preventDefault();
}
function init() {
if (initialized) return;
$(document).on('submit', '[name="dczcomments__box__form"]', onBoxSubmit);
$(document).on('vjCommentBoxCancel', '[name="dczcomments__box__form"]', onBoxCancel);
$(document).on('click', '[name="dczcomments__box__cancel"]', onBoxCancel);
initialized = true;
}
export default class CommentBox extends DOMAttachedObject {
static DOMAttachKey = 'vjCommentBoxInstance';
constructor($dom, options = {}) {
super($dom);
init();
this.$box = $template.clone();
this.$box.data('instance', this);
this.options = {
initialText: null,
mode: null,
form: null,
onCancel: () => null,
...options,
};
if (this.options.initialText) this.setText(this.options.initialText);
if (this.options.mode) {
const submitButton = this.$box.find('.dczcomments__box__submit');
submitButton.val(submitButton.attr(`data-value-${this.options.mode}`));
}
}
getTextareaHandler() {
const $textarea = this.$box.find('textarea');
return TextareaHandler.getOrConstruct($textarea);
}
focus() {
this.getTextareaHandler().focus();
return this;
}
setText(text) {
this.getTextareaHandler().val(text);
return this;
}
getText() {
return this.getTextareaHandler().val();
}
insertText(text) {
const handler = this.getTextareaHandler();
handler.val(handler.val() + text);
return this;
}
appendTo($dom) {
this.$box.appendTo($dom);
this.$box.trigger('vjContentNew');
return this;
}
async onSubmit() {
try {
await request.post('', {
...this.options.form,
content: this.getText(),
});
window.location.reload();
} catch (error) {
Notification.error(error.message);
}
}
async onCancel(ev) {
await this.options.onCancel(ev);
this.$box.remove();
this.detach();
}
}
_.assign(CommentBox, DOMAttachedObject);
@@ -0,0 +1,206 @@
import 'jquery.easing';
import { AutoloadPage } from 'vj/misc/Page';
import CommentBox from 'vj/components/discussion/CommentBox';
import { ConfirmDialog } from 'vj/components/dialog';
import delay from 'vj/utils/delay';
import { slideDown, slideUp } from 'vj/utils/slide';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const $replyTemplate = $('.commentbox-container').eq(0).clone();
function createReplyContainer($parent) {
const $container = $replyTemplate
.clone()
.hide()
.prependTo($parent.find('.commentbox-reply-target').eq(0))
.trigger('vjContentNew');
return $container.find('.commentbox-placeholder');
}
async function showReplyContainer($parent) {
const $container = $parent.find('.commentbox-container');
// TODO: fix ugly hack. cannot get $container rect because it is invisible
const rect = $container.parent()[0].getBoundingClientRect();
const rectBody = document.body.getBoundingClientRect();
if (rect.top < 100 || rect.top + 100 > window.innerHeight) {
const targetScrollTop = rect.top - rectBody.top - window.innerHeight * 0.382;
$('html, body').stop().animate({ scrollTop: targetScrollTop }, 400, 'easeOutCubic');
await delay(300);
// delay duration is set smaller than animation duration intentionally
}
$container.css('opacity', 0);
await slideDown($container, 300);
await $container.transition({ opacity: 1 }, { duration: 200 }).promise();
$container.removeAttr('style');
}
async function destroyReplyContainer($parent) {
const $container = $parent.find('.commentbox-container');
$container.css('opacity', 1);
await $container.transition({ opacity: 0 }, { duration: 200 }).promise();
await slideUp($container, 300);
$container.remove();
}
function onClickDummyBox(ev) {
const $evTarget = $(ev.currentTarget);
if (CommentBox.get($evTarget)) {
CommentBox
.get($evTarget)
.focus();
return;
}
const $mediaBody = $evTarget.closest('.media__body');
const opt = {
form: JSON.parse($evTarget.attr('data-form')),
mode: 'comment',
onCancel: () => {
$mediaBody.removeClass('is-editing');
},
};
$mediaBody.addClass('is-editing');
CommentBox
.getOrConstruct($evTarget, opt)
.appendTo($mediaBody.find('.commentbox-placeholder').eq(0))
.focus();
}
async function onCommentClickReplyComment(ev, options = {}) {
const $evTarget = $(ev.currentTarget);
if (CommentBox.get($evTarget)) {
// If comment box is already expanded,
// we should insert "initialText"
CommentBox
.get($evTarget)
.insertText(options.initialText || '')
.focus();
return;
}
const $mediaBody = $evTarget.closest('.media__body');
const opt = {
initialText: '',
mode: 'reply',
...options,
onCancel: async () => {
await destroyReplyContainer($mediaBody);
},
};
const cbox = CommentBox
.getOrConstruct($evTarget, {
form: JSON.parse($evTarget.attr('data-form')),
...opt,
})
.appendTo(createReplyContainer($mediaBody));
await showReplyContainer($mediaBody);
cbox.focus();
}
async function onCommentClickReplyReply(ev) {
const $evTarget = $(ev.currentTarget);
const $mediaBody = $evTarget.closest('.media__body');
const username = $mediaBody
.find('.user-profile-name').eq(0)
.text();
$evTarget
.closest('.dczcomments__item')
.find('[name="dczcomments__op-reply-comment"]').eq(0)
.trigger('click', { initialText: `@${username}: ` });
}
async function onCommentClickEdit(mode, ev) {
const $evTarget = $(ev.currentTarget);
if (CommentBox.get($evTarget)) {
CommentBox
.get($evTarget)
.focus();
return;
}
const $mediaBody = $evTarget.closest('.media__body');
const raw = await request.get(
$mediaBody.find('.typo').eq(0).attr('data-raw-url'),
{},
{ dataType: 'text' },
);
const opt = {
initialText: raw,
form: JSON.parse($evTarget.attr('data-form')),
mode,
onCancel: () => {
$mediaBody.removeClass('is-editing');
},
};
$mediaBody.addClass('is-editing');
CommentBox
.getOrConstruct($evTarget, opt)
.appendTo($mediaBody.find('.commentbox-edit-target').eq(0))
.focus();
}
function onCommentClickEditComment(ev) {
return onCommentClickEdit('comment-update', ev);
}
function onCommentClickEditReply(ev) {
return onCommentClickEdit('reply-update', ev);
}
async function onCommentClickDelete(type, ev) {
const message = (type === 'comment')
? 'Confirm deleting this comment? Its replies will be deleted as well.'
: 'Confirm deleting this reply?';
const action = await new ConfirmDialog({
$body: tpl`
<div class="typo">
<p>${i18n(message)}</p>
</div>`,
}).open();
if (action !== 'yes') {
return;
}
const $evTarget = $(ev.currentTarget);
const form = JSON.parse($evTarget.attr('data-form'));
await request.post('', form);
window.location.reload();
}
function onCommentClickDeleteComment(ev) {
onCommentClickDelete('comment', ev);
}
function onCommentClickDeleteReply(ev) {
onCommentClickDelete('reply', ev);
}
const commentsPage = new AutoloadPage('commentsPage', () => {
$(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
$(document).on('click', '[name="dczcomments__op-reply-comment"]', onCommentClickReplyComment);
$(document).on('click', '[name="dczcomments__op-reply-reply"]', onCommentClickReplyReply);
$(document).on('click', '[name="dczcomments__op-edit-comment"]', onCommentClickEditComment);
$(document).on('click', '[name="dczcomments__op-edit-reply"]', onCommentClickEditReply);
$(document).on('click', '[name="dczcomments__op-delete-comment"]', onCommentClickDeleteComment);
$(document).on('click', '[name="dczcomments__op-delete-reply"]', onCommentClickDeleteReply);
});
export default commentsPage;
@@ -0,0 +1,125 @@
$upvote-color = green
$downvote-color = red
.dczcomments__item .is-editing
[name="dczcomments__dummy-box"]
display: none
> .typo
display: none
.dczcomments__box .textbox
height: rem(180px)
resize: vertical
.textbox[name="dczcomments__dummy-box"]
height: rem(60px)
resize: none
font-size: rem($font-size-title)
color: $comment-placeholder-color
padding: rem(10px)
.dczcomments__item,
.dczcomments__reply
border-top: 1px solid $table-border-color
>.media
padding: rem(20px 0)
&:first-child
border-top: 0
.dczcomments__reply
&:last-child
>.media
padding-bottom: 0
.dczcomments__supplementary
float: left
margin-bottom: rem(5px)
.dczcomments__operations
font-size: rem($font-size)
float: right
vertical-align: top
color: $comment-op-color
a, a:visited
color: $comment-op-link-color
margin-left: rem(3px)
a:hover
color: $primary-color
text-decoration: none
.view--solution .dczcomments__replies
margin-left: rem(50px)
.dczcomments__item
.vote-number
width: rem(82px)
text-align: center
font-size: rem(25px)
font-weight: 300
margin-bottom: rem(10px)
.vote-op
position: relative
border: 1px solid transparent
border-radius: rem(14px)
transition: border-color .2s linear, opacity .2s linear
opacity: 0.5
&>form
width: rem(40px)
float: left
position: relative
&:hover .vote-op
opacity: 1
border-color: $supplementary-border-color
.vote-button
appearance: none
border: 0
background: none
width: 100%
outline: 0
font-size: rem($font-size-icon)
line-height: rem(26px)
color: #AAA
transition: color .2s linear, background-color .2s linear, box-shadow .2s linear
&.upvote
border-radius: rem(13px 0 0 13px)
padding-left: rem(7px)
&:hover
background: $upvote-color
color: #FFF
box-shadow: 0 2px 10px rgba($upvote-color, 0.4)
&.active
color: $upvote-color
&.downvote
border-radius: rem(0 13px 13px 0)
padding-right: rem(7px)
&:hover
background: $downvote-color
color: #FFF
box-shadow: 0 2px 10px rgba($downvote-color, 0.4)
&.active
color: $downvote-color
&.active
background: transparent !important
box-shadow: none !important
cursor: default
+mobile()
.dczcomments__item
.media__left
display: none
@@ -0,0 +1,17 @@
.discussion-node-tag
font-size: rem($font-size-small)
display: inline-block
line-height: rem(22px)
padding: rem(0 10px)
vertical-align: middle
border-radius: rem(12px)
border: 1px solid $supplementary-border-color
&:hover
background: $primary-color
color: #FFF !important
text-decoration: none
border-color: $primary-color
.icon
margin-right: rem(5px)
@@ -0,0 +1,40 @@
.drop-element,
.drop-element:after,
.drop-element:before,
.drop-element *,
.drop-element *:after,
.drop-element *:before
box-sizing: border-box
.drop-element
position: absolute
display: none
opacity: 0
transition: opacity .1s ease-in-out
z-index: 1000 // managed
&.drop-open
display: block
&.drop-open-transitionend
display: block
&.drop-after-open
opacity: 1
.drop-target-attached-top.drop-target-attached-left .drop-content
transform-origin: -10% 110%
.drop-target-attached-top.drop-target-attached-center .drop-content
transform-origin: 50% 110%
.drop-target-attached-top.drop-target-attached-right .drop-content
transform-origin: 110% 110%
.drop-target-attached-bottom.drop-target-attached-left .drop-content
transform-origin: -10% -10%
.drop-target-attached-bottom.drop-target-attached-center .drop-content
transform-origin: 50% -10%
.drop-target-attached-bottom.drop-target-attached-right .drop-content
transform-origin: 110% -10%
.drop-target-attached-center.drop-target-attached-right .drop-content
transform-origin: -10% 50%
.drop-target-attached-center.drop-target-attached-left .drop-content
transform-origin: 110% 50%
@@ -0,0 +1,55 @@
import Drop from 'tether-drop';
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import responsiveCutoff from 'vj/breakpoints.json';
import zIndexManager from 'vj/utils/zIndexManager';
import { isBelow } from 'vj/utils/mediaQuery';
export default class Dropdown extends DOMAttachedObject {
static DOMAttachKey = 'vjDropdownInstance';
static DOMAttachSelector = '[data-dropdown-target]';
constructor($dom, options = {}) {
if ($dom.attr('data-dropdown-trigger-desktop-only') !== undefined) {
if (isBelow(responsiveCutoff.mobile)) {
super(null);
return;
}
}
super($dom);
this.options = {
target: null,
position: $dom.attr('data-dropdown-pos') || 'bottom left',
...options,
};
this.dropInstance = new Drop({
target: $dom[0],
classes: 'dropdown',
content: this.options.target || $.find($dom.attr('data-dropdown-target'))[0],
position: this.options.position,
openOn: 'hover',
constrainToWindow: true,
constrainToScrollParent: false,
});
this.dropInstance.on('open', this.onDropOpen.bind(this));
this.dropInstance.on('close', this.onDropClose.bind(this));
}
detach() {
super.detach();
this.dropInstance.destroy();
}
onDropOpen() {
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
this.$dom.trigger('vjDropdownShow');
}
onDropClose() {
this.$dom.trigger('vjDropdownHide');
}
}
_.assign(Dropdown, DOMAttachedObject);
@@ -0,0 +1,8 @@
import { AutoloadPage } from 'vj/misc/Page';
import Dropdown from './Dropdown';
const dropdownPage = new AutoloadPage('dropdownPage', () => {
Dropdown.registerLifeCycleHooks();
});
export default dropdownPage;
@@ -0,0 +1,24 @@
.dropdown-target
display: none
.drop .dropdown-target
display: block
.dropdown
.menu
min-width: rem(150px)
box-shadow: $menu-drop-shadow
background: $menu-drop-bg-color
font-size: rem($font-size-small)
.menu__link
padding: rem(8px 10px)
.dropdown
.drop-content
transition: transform .1s ease-in-out
transform: scale(0.9) translateZ(0)
&.drop-after-open
.drop-content
transform: scale(1) translateZ(0)
@@ -0,0 +1,6 @@
.emoji
width: 1em
height: 1em
display: inline-block
background-size: contain
vertical-align: baseline
@@ -0,0 +1,21 @@
import emojify from 'emojify.js';
import { AutoloadPage } from 'vj/misc/Page';
function runEmojify($container) {
if ($container.is('[data-emoji-enabled]')) {
emojify.run($container[0]);
return;
}
$container.find('[data-emoji-enabled]').get().forEach((element) => emojify.run(element));
}
const emojifyPage = new AutoloadPage('emojifyPage', () => {
emojify.setConfig({
img_dir: `${UiContext.cdn_prefix}img/emoji`,
});
runEmojify($('body'));
$(document).on('vjContentNew', (e) => runEmojify($(e.target)));
});
export default emojifyPage;
@@ -0,0 +1,31 @@
import { AutoloadPage } from 'vj/misc/Page';
import { isBelow } from 'vj/utils/mediaQuery';
import { slideUp, slideDown } from 'vj/utils/slide';
import responsiveCutoff from 'vj/breakpoints.json';
const footerPage = new AutoloadPage('footerPage', () => {
if ($('.footer').length === 0) {
return;
}
$('.footer__category.expandable > h1').click(async (ev) => {
if (!isBelow(responsiveCutoff.mobile)) {
return;
}
const $category = $(ev.currentTarget).closest('.footer__category');
const $list = $category.find('.footer__category__expander');
if ($category.hasClass('animating')) {
return;
}
$category.addClass('animating');
if ($category.hasClass('expanded')) {
$category.removeClass('expanded');
await slideUp($list, 300, { opacity: 1 }, { opacity: 0 });
} else {
$category.addClass('expanded');
await slideDown($list, 300, { opacity: 0 }, { opacity: 1 });
}
$category.removeClass('animating');
});
});
export default footerPage;
@@ -0,0 +1,94 @@
$footer-extra-link-gap = 20px
.footer
background: $footer-bg-color
color: $text-1-color
h1
font-size: rem($font-size-title)
+mobile()
padding: rem($grid-padding)
cursor: pointer
font-size: rem($font-size-secondary)
.footer__category
padding-top: rem(30px)
padding-bottom: rem(50px)
+mobile()
padding-top: rem($grid-padding)
padding-bottom: rem($grid-padding)
border-bottom: 1px solid darken($footer-bg-color, 5%)
&.expandable
padding: 0
.expand-icon
display: none
+mobile()
display: inline-block
&.expanded .expand-icon
transform: rotate(180deg)
.footer__category__list
margin-top: rem(10px)
margin-bottom: rem(10px)
font-size: rem($font-size-small)
line-height: 2
+mobile()
margin-top: 0
padding-left: rem($grid-padding)
padding-right: rem($grid-padding)
.footer__category__expander
+mobile()
display: none
+above(rupture.mobile-cutoff)
display: block !important
.footer__extra-left
float: left
.footer__extra-right
float: right
.footer__extra-link
padding: rem(20px 0)
font-size: rem($font-size-small)
.footer__extra-link-item
float: left
line-height: 2
.footer__extra-left .footer__extra-link-item
margin-right: rem($footer-extra-link-gap)
.footer__extra-right .footer__extra-link-item
margin-left: rem($footer-extra-link-gap)
.layout--immersive
.footer
background: none
color: #FFF
a, a:visited, a:active
color: #FFF
a:hover
color: #FFF
+mobile()
.footer__extra-left, .footer__extra-right
float: none
display: block
.footer__extra-link-item
float: none
display: inline-block
margin-left: 0
margin-right: $footer-extra-link-gap
@@ -0,0 +1,82 @@
@import './var.inc.styl'
.button
font-size: rem($font-size-secondary)
line-height: rem($form-control-height - 2)
background-color: lighten($default-color, 50%)
border: 1px solid $default-color
height: rem($form-control-height)
padding: rem(0 25px)
display: inline-block
outline: 0
margin: 0 0 1rem
transition: background-color .2s, box-shadow .5s
transition-timing-function: ease-out-sine
cursor: pointer
user-select: none
&, &:visited, &:hover
&, .typo &
color: $text-1-color
&:hover, &:focus
text-decoration: none
&:not(.disabled)
&:hover, &:focus
background-color: darken($default-color, 5%)
border-color: darken($default-color, 5%)
box-shadow: 0 4px 15px rgba(darken($default-color, 10%), 0.5)
&:focus
border-color: $input-focus-color
&.expanded
padding-left: 0
padding-right: 0
text-align: center
width: 100%
&.rounded
border-radius: rem($form-control-height / 2)
&.primary
border-color: $primary-color
background-color: $primary-color
&, &:visited, &:hover
&, .typo &
color: #FFF
&:not(.disabled)
&:hover
background-color: lighten(saturate($primary-color, 30%), 10%)
border-color: saturate($primary-color, 70%)
box-shadow: 0 4px 15px rgba(saturate($primary-color, 70%), 0.3)
&.disabled
cursor: default
opacity: 0.5
&.compact
margin-bottom: 0
height: rem($compact-control-height)
line-height: rem($compact-control-height - 2)
padding: rem(0 15px)
.button.inverse
line-height: rem($form-control-height - 4)
border: 2px solid #FFF
background: none
&, &:visited
&, .typo &
color: #FFF
&:not(.disabled)
&:hover, &:focus
border-color: $immersive-primary-color
background: none
box-shadow: none
&, .typo &
color: $immersive-primary-color
@@ -0,0 +1,90 @@
@import './var.inc.styl'
$checkbox-size = 16px
.checkbox
font-size: rem($font-size-secondary)
line-height: 1.4
border: 0
display: inline-block
vertical-align: middle
cursor: pointer
user-select: none
color: lighten($text-1-color, 30%)
margin: $input-margin
padding: ((rem($form-control-height) - (@line-height * @font-size)) / 2) 0
&:hover
color: $text-1-color
input
appearance: none
border: 1px solid #D0D0D0
display: inline-block
border-radius: 1px
vertical-align: top
width: rem($checkbox-size)
height: rem($checkbox-size)
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
margin-right: rem(5px)
position: relative
outline: 0
transition: background .1s linear
background: none
cursor: pointer
&:after
content: ''
width: rem(10px)
height: rem(6px)
position: absolute
top: rem(3px)
left: rem(2px)
border: 2px solid #FFF
border-top: none
border-right: none
background: transparent
transform: rotate(-65deg) scale(0.5)
opacity: 0
&:enabled:hover
border-color: rgba($secondary-color, 0.7)
&:checked
background: $secondary-color
border-color: $secondary-color
&:checked:after
opacity: 1
transform: rotate(-45deg)
&:disabled
opacity: 0.5
&.compact
margin-bottom: 0
line-height: 1.2
padding: ((rem($compact-control-height) - (@line-height * @font-size)) / 2) 0
input
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
margin-right: rem(5px)
label.checkbox.inverse
color: rgba($immersive-text-color, 0.8)
input
color: $immersive-text-color
border-color: rgba($immersive-text-color, 0.4)
&:disabled, &:read-only
color: rgba($immersive-text-color, 0.6)
&:enabled:read-write:hover
border-color: rgba($immersive-text-color, 0.8)
&:enabled:read-write:focus
border-color: $immersive-primary-color
&.focus
color: $immersive-primary-color
@@ -0,0 +1,15 @@
import { AutoloadPage } from 'vj/misc/Page';
const formPage = new AutoloadPage('formPage', () => {
$(document).on('vjFormDisableUpdate', 'input, select, textarea', (ev) => {
const $input = $(ev.currentTarget);
const $formItem = $input.closest('.form__item');
if ($input.prop('disabled')) {
$formItem.addClass('is--disabled');
} else {
$formItem.removeClass('is--disabled');
}
});
});
export default formPage;
@@ -0,0 +1,199 @@
// Extracted from Foundation framework
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea
color: inherit
/* 1 */
font: inherit
/* 2 */
margin: 0
/* 3 */
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button
overflow: visible
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select
text-transform: none
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"],
input[type="reset"],
input[type="submit"]
appearance: button
/* 2 */
cursor: pointer
/* 3 */
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled]
cursor: not-allowed
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner
border: 0
padding: 0
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input
line-height: normal
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"]
box-sizing: border-box
/* 1 */
padding: 0
/* 2 */
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button
height: auto
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
*/
input[type="search"]
appearance: textfield
/* 1 */
box-sizing: content-box
/* 2 */
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration
-webkit-appearance: none
/**
* Define consistent border, margin, and padding.
* [NOTE] We don't enable this ruleset in Foundation, because we want the <fieldset> element to have plain styling.
*/
/* fieldset
border: 1px solid #c0c0c0
margin: 0 2px
padding: 0.35em 0.625em 0.75em
*/
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend
border: 0
/* 1 */
padding: 0
/* 2 */
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea
overflow: auto
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup
font-weight: bold
.form--inline
display: inline
textarea
height: auto
border-radius: 0px
resize: vertical
select
width: 100%
border-radius: 0px
button
appearance: none
background: transparent
padding: 0
border: 0
border-radius: 0px
line-height: 1
[data-whatinput='mouse'] button
outline: 0
label
display: block
margin: 0
font-size: rem($font-size-secondary)
line-height: 1.8
color: $label-color
input, textarea, select
display: block
.help-text
margin-top: -0.5rem
margin-bottom: 1rem
font-size: rem($font-size-small)
color: $help-text-color
.form__item
transition: opacity .1s linear
&.is--disabled
opacity: 0.3
:disabled
opacity: 1
@@ -0,0 +1,45 @@
import { AutoloadPage } from 'vj/misc/Page';
const lastSelectionByGroup = {};
function isInGroup(group) {
return (idx, element) => element.getAttribute('data-checkbox-group') === group;
}
function onRangeRoleCheckboxClick(ev) {
const $current = $(ev.currentTarget);
const targetGroup = $current.attr('data-checkbox-group');
const $targets = $('[type="checkbox"]')
.filter(isInGroup(targetGroup))
.filter(':visible')
.filter(':not(:disabled)');
if (ev.shiftKey && lastSelectionByGroup[targetGroup]) {
const destCheck = lastSelectionByGroup[targetGroup].prop('checked');
const from = $targets.index(lastSelectionByGroup[targetGroup]);
const to = $targets.index($current);
const start = Math.min(from, to);
const end = Math.max(from, to) + 1;
$targets
.slice(start, end)
.prop('checked', destCheck);
}
lastSelectionByGroup[targetGroup] = $current;
}
function onToggleRoleCheckboxClick(ev) {
const $current = $(ev.currentTarget);
const targetGroup = $current.attr('data-checkbox-toggle');
const $targets = $('[type="checkbox"]')
.filter(isInGroup(targetGroup))
.filter(':visible')
.filter(':not(:disabled)');
const destCheck = $current.prop('checked');
$targets.prop('checked', destCheck);
}
const multiSelectCheckboxPage = new AutoloadPage('multiSelectCheckboxPage', () => {
$(document).on('click', '[data-checkbox-range]', onRangeRoleCheckboxClick);
$(document).on('click', '[data-checkbox-toggle]', onToggleRoleCheckboxClick);
});
export default multiSelectCheckboxPage;
@@ -0,0 +1,62 @@
@import './var.inc.styl'
$radiobox-size = 16px
.radiobox
display: block
font-size: rem($font-size-secondary)
line-height: 1.4
border: 0
cursor: pointer
user-select: none
color: lighten($text-1-color, 30%)
margin: $input-multi-line-margin
&:hover
color: $text-1-color
input
display: inline-block
.radiobox-container
margin: $input-margin
// Image Radio Box
.radiobox.with-image
margin: $input-image-radio-margin
.radiobox__image-container
cursor: pointer
border: 2px solid #DDD
transition: border-color .1s linear, opacity .1s linear
padding: 1px
overflow: hidden
position: relative
opacity: 0.7
&:hover
border: 2px solid $primary-color
opacity: 1
&:after
position: absolute
width: rem(20px)
height: rem(20px)
right: rem(-10px)
top: rem(-10px)
transform: translateX(10px) translateY(-10px) rotate(45deg)
background: $highlight-color
content: ''
transition: transform .1s ease-out-cubic
input
visibility: hidden
position: absolute
&:checked + .radiobox__image-container
border: 2px solid $highlight-color
opacity: 1
&:after
transform: rotate(45deg)
@@ -0,0 +1,29 @@
@import './var.inc.styl'
.select
form-styles()
padding-right: 1.1rem
background-color: $input-background-color
background-image: url('~vj/misc/icons/' + $icon-expand_more-file)
background-size: 16px 16px
background-position: right -1rem center
background-origin: content-box
background-repeat: no-repeat
outline: $input-outline
transition: outline-color .2s, border-color .2s
transition-timing-function: ease-out-cubic
&:focus
border-color: $input-focus-border-color
outline: $input-focus-outline
outline-offset: 0
&:disabled
opacity: 0.5
.select-container.compact .select, .select.compact
margin-bottom: 0
height: rem($compact-control-height)
line-height: rem($compact-control-height - 2)
padding-top: 0
padding-bottom: 0
@@ -0,0 +1,20 @@
import { AutoloadPage } from 'vj/misc/Page';
const textboxPage = new AutoloadPage('textboxPage', () => {
$(document).on('focusin', '.textbox.material input', (ev) => {
$(ev.currentTarget).parent().addClass('focus');
});
$(document).on('focusout', '.textbox.material input', (ev) => {
$(ev.currentTarget).parent().removeClass('focus');
});
const $focusElement = $(document.activeElement);
if ($focusElement.prop('tagName') === 'INPUT'
&& $focusElement.parent().is('.textbox.material')
) {
$focusElement.focusin();
}
});
export default textboxPage;
@@ -0,0 +1,72 @@
@import './var.inc.styl'
// Normal Textbox
input.textbox, textarea.textbox
form-styles()
background-color: $input-background-color
outline: $input-outline
transition: outline-color .2s, border-color .2s
transition-timing-function: ease-out-cubic
&:focus
border-color: $input-focus-border-color
outline: $input-focus-outline
outline-offset: 0
// Overwrite, for IE compatibility
&:read-only
outline: 0
&:disabled
opacity: 0.5
textarea.textbox
height: rem(200px)
.data-table
input.textbox, textarea.textbox
margin-bottom: 0
// Material Textbox
label.textbox.material
input
form-styles()
border: 0
background: none
outline: 0
padding-left: 0
padding-right: 0
color: $text-1-color
border-bottom: 2px solid rgba($text-1-color, 0.4)
transition: border-color .2s ease-out-cubic
&:disabled, &:read-only
color: rgba($text-1-color, 0.6)
&:enabled:read-write:hover
border-color: rgba($text-1-color, 0.8)
&:enabled:read-write:focus
border-color: $input-focus-border-color
&.focus
color: $input-focus-border-color
label.textbox.material.inverse
color: rgba($immersive-text-color, 0.8)
input
color: $immersive-text-color
border-color: rgba($immersive-text-color, 0.4)
&:disabled, &:read-only
color: rgba($immersive-text-color, 0.6)
&:enabled:read-write:hover
border-color: rgba($immersive-text-color, 0.8)
&:enabled:read-write:focus
border-color: $immersive-primary-color
&.focus
color: $immersive-primary-color
@@ -0,0 +1,14 @@
form-styles()
appearance: none
display: block
width: 100%
font-size: rem($font-size-secondary)
margin: $input-margin
height: rem($form-control-height)
line-height: 1.2
padding: rem(5px)
border: $input-border
&.inline
display: inline-block
width: auto
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,176 @@
$header-gap-mini = ($header-bg-height-mini - $nav-item-height - $header-logo-height) / 2
$header-gap-mini-domain = ($header-bg-height-mini - $nav-item-height - $header-logo-domain-lh - $header-logo-system-lh) / 2
.global-notification
padding: rem(20px 0)
background: #FFF
&.warning
background: #ffe1a6
.header__options
background-image: url(./header-background.png)
background-image: url(./header-background@2x.png)
.header
position: relative
height: rem($header-bg-height)
background: #5eafca no-repeat center top
background-size: cover
transition: height .5s
transition-timing-function: ease-out-cubic
overflow: hidden
+retina()
background-size: rem($header-bg-width $header-bg-height)
&.mini
height: rem($header-bg-height-mini)
+mobile()
background: #FFF
height: auto !important
.header__content
display: none
.header__logo__options
background-image: url(./header-logo.png)
background-image: url(./header-logo@2x.png)
background-image: url(./header-logo-summer.png)
background-image: url(./header-logo-summer@2x.png)
.header__logo
display: block
width: rem($header-logo-width-real)
height: rem($header-logo-height + $header-gap-mini + $nav-item-height)
background: no-repeat
background-size: rem($header-logo-width-real) rem($header-logo-height-real)
background-position: 0 rem($header-gap-mini + $nav-item-height - ($header-logo-height-real - $header-logo-height))
transition: transform .5s, opacity .5s
transition-timing-function: ease-out-cubic
transform-origin: 0 0
opacity: 1
+mobile()
display: inline-block
.header__logo--domain
font-size: rem(40px)
margin-top: rem($header-gap-mini-domain + $nav-item-height)
height: rem($header-logo-domain-lh)
line-height: rem($header-logo-domain-lh)
a
text-shadow: 1px 1px 5px rgba(#000, 0.4)
&, &:hover, &:visited, &:link
color: #FFF
text-decoration: none
.header__logo--system
line-height: rem($header-logo-system-lh)
color: #FFF
font-size: rem($font-size-small)
a
opacity: 0.8
&, &:hover, &:visited, &:link
text-decoration: none
color: #FFF
&:hover
opacity: 1
text-decoration: underline
.header--collapsed
#panel
padding-top: $header-bg-height
.header
position: absolute
left: 0
top: 0
width: 100%
height: 0
.header__logo
transform: scale(0)
opacity: 0
.location
position: absolute
left: 0
bottom: 0
width: 100%
height: rem($header-layer-height)
background: rgba(#000, 0.5)
+mobile()
position: relative
height: auto
.header__hamburger
display: none
+mobile()
display: block
.header.mini .location
display: none
.location-path
color: rgba(#FFF, 0.3)
line-height: rem(30px)
margin-top: rem(10px)
a, a:visited
color: rgba(#FFF, 0.6)
a:hover
color: rgba(#FFF, 0.9)
.location-current
color: #9ad4f1
line-height: rem(40px)
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
+mobile()
color: #91fcff
font-size: 1.7rem
white-space: normal
.header--mobile
display: none
.header--mobile, .location
+mobile()
display: block
background: #56758f
color: #FFF
line-height: rem($nav-item-height)
font-size: rem($font-size-title)
padding: rem(10px 0)
a, a:visited, a:hover
color: #FFF
.header--mobile__domain.is--system
display: block
width: $nav-logo-width // no rem
height: $nav-logo-height // no rem
margin-top: rem(10px)
+retina()
background-size: $nav-logo-width $nav-logo-height // no rem
.header--mobile__system
margin-left: rem(10px)
font-size: rem($font-size-secondary)
&, &:hover, &:visited
color: $supplementary-color
.header__hamburger
background: none
border: 0
border-radius: 0
outline: 0
@@ -0,0 +1,12 @@
@css {
span.cm-comment { color: #008000; }
span.cm-keyword, span.cm-variable-3 { line-height: 1em; color: #00f; }
span.cm-string { color: #a31515; }
span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; }
span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; }
span.cm-variable { color: black; }
span.cm-meta { color: #2b91af; }
span.cm-link { color: #3a3; }
.CodeMirror-activeline-background { background: #e8f2ff; }
.CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
}
@@ -0,0 +1,14 @@
import { AutoloadPage } from 'vj/misc/Page';
const highlighterPage = new AutoloadPage('highlighterPage', () => {
import('./prismjs').then((module) => {
const prismjs = module.default;
function runHighlight($container) {
prismjs.highlightBlocks($container);
}
runHighlight($('body'));
$(document).on('vjContentNew', (e) => runHighlight($(e.target)));
});
});
export default highlighterPage;
@@ -0,0 +1,122 @@
/*
VS theme by Andrew Lock
https://github.com/PrismJS/prism-themes/blob/master/themes/prism-vs.css
*/
pre, code
background: $syntax-hl-bg-color
pre > code
background: none
line-height: 1.5
pre
position: relative
padding: rem(5px)
code
color: $code-color
font-size: rem($font-size-secondary)
pre > code
color: black
text-align: left
white-space: pre-wrap
word-spacing: normal
word-break: normal
tab-size: 4
hyphens: none
[data-syntax-hl-show-line-number] pre.line-numbers > code
white-space: pre
.syntax-hl
.token
&.comment, &.prolog, &.doctype, &.cdata
color: #008000
font-style: italic
&.namespace
opacity: 0.7
&.string
color: #a31515
&.punctuation, &.operator
color: #393A34
&.url, &.symbol, &.number, &.boolean, &.inserted
color: #36acaa
&.atrule, &.keyword, &.attr-value, .language-autohotkey&.selector, .language-json&.boolean, .language-json&.number
color: #0000ff
&.function
color: #393A34
&.deleted, .language-autohotkey&.tag
color: #9a050f
&.selector, .language-autohotkey&.keyword
color: #00009f
&.important, &.bold
font-weight: bold
&.italic
font-style: italic
&.class-name, .language-json&.property
color: #2B91AF
&.tag, &.selector
color: #800000
&.attr-name, &.property, &.regex, &.entity
color: #ff0000
&.directive.tag .tag
background: #ffff00
color: #393A34
&.macro
&, .token
color: #2b91af
&.builtin
color: #077
.toolbar
position: absolute
white-space: no-wrap
right: 0
top: 0
padding: rem(5px)
background: rgba($syntax-hl-bg-color, 0.8)
font-family: $primary-font-family
font-size: rem($font-size-small)
opacity: 0
pointer-events: none
transition: opacity .1s linear
user-select: none
&:hover .toolbar
opacity: 1
pointer-events: all
[data-syntax-hl-show-line-number]
pre
padding-left: 3.8em
counter-reset: linenumber
pre > code
position: relative
.line-numbers-rows
position: absolute
pointer-events: none
top: 0
font-size: 100%
left: -3.8em
width: 3em
letter-spacing: -1px
user-select: none
span
pointer-events: none
display: block
counter-increment: linenumber
&:before
content: counter(linenumber)
color: #ddd
display: block
padding-right: 0.8em
text-align: right
@@ -0,0 +1,15 @@
// The following content is extracted from:
// https://github.com/codemirror/CodeMirror/blob/master/mode/meta.js
export default [
{ name: 'C', ext: ['c', 'h'] },
{ name: 'C++', ext: ['cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx'] },
{ name: 'Go', ext: ['go'] },
{ name: 'Haskell', ext: ['hs'] },
{ name: 'Java', ext: ['java'] },
{ name: 'JavaScript', ext: ['js'] },
{ name: 'Pascal', ext: ['p', 'pas'] },
{ name: 'PHP', ext: ['php', 'php3', 'php4', 'php5', 'php7', 'phtml'] },
{ name: 'Python', ext: ['BUILD', 'bzl', 'py', 'pyw'] },
{ name: 'Rust', ext: ['rs'] },
];
@@ -0,0 +1,76 @@
/*
To add a new language to highlight:
1. Add language in babel in package.json
2. Add new import statement in `components/cmeditor/vjcmeditor.js`
3. Add new import statement in `components/scratchpad/ScratchpadEditorContainer.js`
4. Add new meta data in `components/highlighter/meta.js`
*/
import Prism from 'prismjs';
import Clipboard from 'clipboard';
import Notification from 'vj/components/notification';
import i18n from 'vj/utils/i18n';
import languageMeta from './meta';
const languageExtMap = {};
// Map possible language names to Prism language name
languageMeta.forEach((meta) => {
for (let i = 0; i < meta.ext.length; ++i) {
if (Prism.languages[meta.ext[i]] !== undefined) {
// eslint-disable-next-line no-param-reassign
meta.target = meta.ext[i];
break;
}
}
meta.ext.forEach((ext) => {
languageExtMap[ext] = meta.target;
});
});
// Copy to Clipboard
try {
Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
const linkCopy = document.createElement('a');
linkCopy.href = 'javascript:;'; // eslint-disable-line no-script-url
linkCopy.textContent = 'Copy';
const clip = new Clipboard(linkCopy, { text: () => env.code });
clip.on('success', () => {
Notification.success(i18n('Content copied to clipboard!'), 1000);
});
clip.on('error', () => {
Notification.error(i18n('Copy failed :('));
});
return linkCopy;
});
} catch (e) {
// snowpack
}
const prismjsApiWrap = {
highlightBlocks: ($dom) => {
$dom.find('pre code').get().forEach((code) => {
const $pre = $(code).parent();
$pre.addClass('syntax-hl');
if ($pre.closest('[data-syntax-hl-show-line-number]')) {
$pre.addClass('line-numbers');
}
// try to map the language name
const language = $(code).attr('class');
const m = (language || '').trim().match(/^language-(.+)$/);
if (m && m[1]) {
const languageName = m[1].toLowerCase();
if (languageExtMap[languageName]) {
$(code).attr('class', `language-${languageExtMap[languageName]}`);
}
}
Prism.highlightElement(code);
});
},
};
export default prismjsApiWrap;
@@ -0,0 +1,28 @@
import { AutoloadPage } from 'vj/misc/Page';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const hitokotoPage = new AutoloadPage('hitokotoPage', () => {
function getHitokoto($containers) {
$containers.get().forEach((container) => {
request.get('https://v1.hitokoto.cn?c=a&c=b&c=c&c=d&c=e&c=f')
.then((hitokoto) => {
const dom = $(tpl`<p>${hitokoto.hitokoto}</p>`);
dom.appendTo(container);
})
.catch((e) => {
console.error(e);
const dom = $(tpl`<p>${i18n('Cannot get hitokoto.')}</p>`);
dom.appendTo(container);
});
});
}
if ($('[name="hitokoto"]')) getHitokoto($('[name="hitokoto"]'));
$(document).on('vjContentNew', (e) => {
const elem = $(e.target).find('[name="hitokoto"]');
if (elem.get) getHitokoto(elem);
});
});
export default hitokotoPage;
@@ -0,0 +1,80 @@
import _ from 'lodash';
import { AutoloadPage } from 'vj/misc/Page';
const KEY_MAP = {
10: 'enter',
13: 'enter',
27: 'esc',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
};
function isHotkeyMatch(sortedHotkeyArr, hotkeyStr) {
const hotkeyDefined = hotkeyStr.toLowerCase().split('+');
return _.isEqual(sortedHotkeyArr, hotkeyDefined.sort());
}
function testElementHotkey(hotkey, $element, attr) {
if (!$element.is(':visible')) {
return;
}
String($element.attr(attr))
.split(',')
.forEach((singleDef) => {
const [defStr, trigger] = singleDef.split(':');
if (isHotkeyMatch(hotkey, defStr)) {
switch (trigger) {
case 'submit':
$element.closest('form').trigger('submit');
break;
case undefined:
$element.trigger('click');
break;
default:
$element.trigger(trigger);
break;
}
}
});
}
const hotkeyPage = new AutoloadPage('hotkeyPage', () => {
$(document).on('keydown', (ev) => {
const hotkey = ['alt', 'ctrl', 'shift'].filter((modifyKey) => ev[`${modifyKey}Key`]);
if (ev.metaKey && !ev.ctrlKey) {
hotkey.push('ctrl');
}
if (KEY_MAP[ev.which] !== undefined) {
hotkey.push(KEY_MAP[ev.which]);
} else {
hotkey.push(String.fromCharCode(ev.which).toLowerCase());
}
hotkey.sort();
// Find all global hotkeys
$('[data-global-hotkey]').get().forEach((element) => {
testElementHotkey(hotkey, $(element), 'data-global-hotkey');
});
// Find all local hotkeys
$(ev.target).parents('[data-hotkey]').get().forEach((element) => {
testElementHotkey(hotkey, $(element), 'data-hotkey');
});
});
});
export default hotkeyPage;
@@ -0,0 +1,13 @@
import { AutoloadPage } from 'vj/misc/Page';
const katexPage = new AutoloadPage('katexPage', () => {
import('katex/dist/contrib/auto-render.min.js').then(({ default: katex }) => {
function runKatex($containers) {
$containers.get().forEach((container) => katex(container));
}
runKatex($('.typo'));
$(document).on('vjContentNew', (e) => runKatex($(e.target).find('.typo')));
});
});
export default katexPage;
@@ -0,0 +1,2 @@
.katex
font-size: 1.2em
@@ -0,0 +1,29 @@
.loader-container
position: absolute
left: 0
width: 100%
top: 0
height: 100%
.loader
text-indent: -9999em
border: rem(5px) solid rgba($primary-color, .2)
border-left: rem(5px) solid $primary-color
animation: load8 1.1s infinite linear
border-radius: 50%
width: rem(50px)
height: rem(50px)
margin-left: rem(-25px)
margin-top: rem(-25px)
left: 50%
top: 50%
position: absolute
@keyframes load8
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
@@ -0,0 +1,193 @@
import _ from 'lodash';
import delay from 'vj/utils/delay';
import Tooltip from 'vj/components/tooltip/Tooltip';
import i18n from 'vj/utils/i18n';
const MARKER_ID = `marker_${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`;
const MARKER_OFFSET = 20;
const MARKER_MAX_DISTANCE = 60;
let markerInstance = null;
function distanceToRect(px, py, rect) {
const cx = (rect.left + rect.right) / 2;
const cy = (rect.top + rect.bottom) / 2;
const dx = Math.max(Math.abs(px - cx) - (rect.right - rect.left) / 2, 0);
const dy = Math.max(Math.abs(py - cy) - (rect.bottom - rect.top) / 2, 0);
return Math.sqrt(dx * dx + dy * dy);
}
class Marker {
static exists() {
return markerInstance && document.getElementById(MARKER_ID);
}
constructor() {
if (Marker.exists()) {
return markerInstance;
}
if (markerInstance) {
markerInstance.destroy();
}
this.$dom = $(`
<div class="marker" id="${MARKER_ID}"><div class="marker__toolbar">
<div class="marker__action" data-color="#ffff00" data-tooltip="${i18n('Mark Yellow')}"><span class="marker__icon icon-yellow"></span></div>
<div class="marker__action" data-color="#47ff6f" data-tooltip="${i18n('Mark Green')}"><span class="marker__icon icon-green"></span></div>
<div class="marker__action" data-color="transparent" data-tooltip="${i18n('Clear Marks')}"><span class="icon icon-erase"></span></div>
</div></div>
`)
.appendTo('body');
this.$dom.find('.marker__toolbar').on('click', '.marker__action', this.onMarkerActionClick.bind(this));
this.$dom.on('mousedown', this.onMarkerMouseDown.bind(this));
this.isOpen = false;
this.bindedHandlers = false;
this._onKeyDown = this.onKeyDown.bind(this);
this._onScroll = _.throttle(this.onScroll.bind(this), 50);
this._onMouseDown = this.onMouseDown.bind(this);
this._onMouseMove = _.throttle(this.onMouseMove.bind(this), 50);
markerInstance = this;
}
markSelection(color) {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
this.$container.attr('contentEditable', true);
if (range) {
// recover ranges
selection.removeAllRanges();
selection.addRange(range);
}
if (!document.execCommand('HiliteColor', false, color)) {
document.execCommand('BackColor', false, color);
}
this.$container.removeAttr('contentEditable');
if (range) {
// remove ranges after marking
selection.removeAllRanges();
}
}
showAtPosition($container, x, y) {
this.$container = $container;
this.$dom.css({
display: 'block',
opacity: 0,
});
const rect = this.$dom[0].getBoundingClientRect();
this.$dom.css({
left: x + MARKER_OFFSET,
top: y - (rect.bottom - rect.top) - MARKER_OFFSET,
});
this.isOpen = true;
this.$dom.addClass('open');
this.updateMarkerOpacity(x, y);
this.bindEventHandlersForClosing();
}
async close() {
if (!this.isOpen) {
return;
}
this.isOpen = false;
this.closeTooltipsImmediately();
this.unbindEventHandlers();
this.$dom
.css({ opacity: 0 })
.removeClass('open');
await delay(200);
if (!this.isOpen) {
this.$dom.hide();
}
}
closeTooltipsImmediately() {
const $elements = this.$dom.find('[data-tooltip]');
$elements.get().forEach((element) => {
const $el = $(element);
const tooltipInstance = Tooltip.get($el);
if (tooltipInstance) {
tooltipInstance.detach();
}
});
}
bindEventHandlersForClosing() {
if (this.bindedHandlers) {
return;
}
$(document).on('keydown', this._onKeyDown);
$(window).on('scroll', this._onScroll);
$(document).on('mousedown', this._onMouseDown);
$(document).on('mousemove', this._onMouseMove);
this.bindedHandlers = true;
}
unbindEventHandlers() {
if (!this.bindedHandlers) {
return;
}
$(document).off('keydown', this._onKeyDown);
$(window).off('scroll', this._onScroll);
$(document).off('mousedown', this._onMouseDown);
$(document).off('mousemove', this._onMouseMove);
this.bindedHandlers = false;
}
onMarkerActionClick(ev) {
const color = $(ev.currentTarget).attr('data-color');
this.markSelection(color);
this.close();
}
onMarkerMouseDown(ev) {
ev.stopPropagation();
ev.preventDefault();
}
onKeyDown() {
this.close();
}
onScroll() {
this.close();
}
onMouseMove(ev) {
this.updateMarkerOpacity(ev.clientX, ev.clientY);
}
onMouseDown() {
this.close();
}
updateMarkerOpacity(x, y) {
const markerRect = this.$dom[0].getBoundingClientRect();
const distance = distanceToRect(x, y, markerRect);
if (distance > MARKER_MAX_DISTANCE) {
this.close();
return;
}
this.$dom.css('opacity', 1 - (distance / MARKER_MAX_DISTANCE));
}
destroy() {
this.close();
this.$dom.remove();
markerInstance = null;
}
}
const MarkerInterface = {
close() {
if (!markerInstance) {
return;
}
markerInstance.close();
},
showAtPosition($container, x, y) {
const marker = new Marker();
marker.showAtPosition($container, x, y);
},
};
export default MarkerInterface;
Loaded 100 of 564 files, more files were not shown because too many files have changed in this diff. Show more