ui-default: init
将ui-default合并至主存储库便于维护
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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==
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"gruntfuggly.todo-tree"
|
"gruntfuggly.todo-tree",
|
||||||
|
"ronnidc.nunjucks",
|
||||||
|
"sysoev.language-stylus"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
backendlib
|
||||||
|
build
|
||||||
|
public
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
.cache/
|
||||||
|
build/
|
||||||
|
common/
|
||||||
|
components/
|
||||||
|
constant/
|
||||||
|
misc/
|
||||||
|
pages/
|
||||||
|
static/
|
||||||
|
utils/
|
||||||
|
__*
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Hydro-UI-Default
|
||||||
|
[](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
|
||||||
|
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fhydro-dev%2Fui-default?ref=badge_large)
|
||||||
@@ -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(/&/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&lock_to_path=0&autoplay=0&autohide_ctrls=0&`
|
||||||
|
+ 'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&'
|
||||||
|
+ '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: 
|
||||||
|
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,
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"mobile": 600,
|
||||||
|
"desktop": 1000,
|
||||||
|
"hd": 1250
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default function root(fn = '.') {
|
||||||
|
return path.resolve(__dirname, '../../', fn);
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
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
|
||||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
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;
|
||||||