This commit is contained in:
masnn committed 2020-04-08 21:10:24 +08:00
1 parent 777a4bc927
commit 9c45527522
396 files changed
+13772

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
const path = require('path');
module.exports = {
"parser": "babel-eslint",
"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
}
},
"settings": {
"import/resolver": {
"webpack": {
"config": {
"resolve": {
"alias": {
"vj": path.resolve(__dirname),
}
}
}
}
}
},
"globals": {
"__webpack_public_path__": true,
"__webpack_require__": true,
"UiContext": true,
"UserContext": true,
"Context": true,
"LOCALES": true
},
"rules": {
"comma-dangle": [
"error",
"always-multiline"
],
"indent": [
"error",
2,
{
"SwitchCase": 0
}
],
"max-len": [
"error",
150
],
"quotes": "warn",
"func-names": "off",
"class-methods-use-this": "off",
"consistent-return": "warn",
"no-restricted-syntax": "warn",
"no-unused-vars": "warn",
"no-console": "off",
"no-continue": "off",
"no-mixed-operators": "off",
"no-plusplus": "off",
"no-underscore-dangle": "off",
"no-await-in-loop": "off",
"no-lonely-if": "off",
"import/first": "off",
"react/prefer-stateless-function": "off",
"react/self-closing-comp": "off",
"react/prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-one-expression-per-line": "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"
}
};
+93
View File
@@ -0,0 +1,93 @@
import 'jquery.transit';
import 'normalize.css/normalize.css';
import 'codemirror/lib/codemirror.css';
import _ from 'lodash';
import 'vj/misc/float.styl';
import 'vj/misc/typography.styl';
import 'vj/misc/textalign.styl';
import 'vj/misc/grid.styl';
import 'vj/misc/slideout.styl';
import 'vj/misc/.iconfont/webicon.styl';
import 'vj/misc/immersive.styl';
import 'vj/misc/structure.styl';
import 'vj/misc/section.styl';
import 'vj/misc/nothing.styl';
import { PageLoader } from 'vj/misc/PageLoader';
import delay from 'vj/utils/delay';
// eslint-disable-next-line camelcase
__webpack_public_path__ = UiContext.cdn_prefix;
const pageLoader = new PageLoader();
const currentPage = pageLoader.getNamedPage(document.documentElement.getAttribute('data-page'));
const includedPages = pageLoader.getAutoloadPages();
function buildSequence(pages, type) {
if (process.env.NODE_ENV !== 'production') {
if (['before', 'after'].indexOf(type) === -1) {
// eslint-disable-next-line quotes
throw new Error(`'type' should be one of 'before' or 'after'`);
}
}
return pages
.filter(p => p && p[`${type}Loading`])
.map(p => ({
page: p,
func: p[`${type}Loading`],
type,
}));
}
async function load() {
const loadSequence = [
...buildSequence(includedPages, 'before'),
...buildSequence([currentPage], 'before'),
...buildSequence(includedPages, 'after'),
...buildSequence([currentPage], 'after'),
];
// eslint-disable-next-line no-restricted-syntax
for (const { page, func, type } of loadSequence) {
if (typeof func !== 'function') {
if (process.env.NODE_ENV !== 'production') {
throw new Error(`The '${type}Loading' function of '${page.name}' is not callable`);
}
continue;
}
if (process.env.NODE_ENV !== 'production') {
console.time(`${page.name}: ${type}Loading`);
}
try {
await func();
} catch (e) {
console.error(`Failed to call '${type}Loading' of ${page.name}\n${e.stack}`);
}
if (process.env.NODE_ENV !== 'production') {
console.timeEnd(`${page.name}: ${type}Loading`);
}
}
const sections = _.map($('.section').get(), (section, idx) => ({
shouldDelay: idx < 5, // only animate first 5 sections
$element: $(section),
}));
// eslint-disable-next-line no-restricted-syntax
for (const { $element, shouldDelay } of sections) {
$element.addClass('visible');
if (shouldDelay) {
await delay(50);
}
}
await delay(500);
// eslint-disable-next-line no-restricted-syntax
for (const { $element } of sections) {
$element.trigger('vjLayout');
}
$(document).trigger('vjPageFullyInitialized');
}
load();
+415
View File
@@ -0,0 +1,415 @@
# Vijos UI Framework
## Responsive Cutoffs
Vijos use the following cutoffs to determine the size of a device.
Small: `[0 - 450px)` (mobile)
Medium: `[450px, 1000px)` (tablet)
Large: `[1000px, -)` (desktop)
## Layout
### Grid
See [Foundation Grid](http://foundation.zurb.com/sites/docs/grid.html)
### Float
See [Foundation Float](http://foundation.zurb.com/sites/docs/float-classes.html)
> NOTE: `.float-center` is not implemented.
## Typography
HTML elements such as headings (`h1`, `h2`, ...), lists (`ol`, `ul`, `li`, ...) and tables does not have margins and paddings by default in order to easy the styling of semantic structures. However you may want suitable space between those elements when they are used to present content to users (for example, problem content). In this case, you need to enable the typography styling by wrapping them with `<div class="typo"></div>`:
```html
<div class="typo">
<h1>Notice</h1>
<p>The content will be well formatted.</p>
<ul>
<li>Item</li>
<li>Item</li>
<li>Item</li>
</ul>
</div>
```
### Text Alignment
```html
<ANY class="text-left"></ANY>
<ANY class="text-center"></ANY>
<ANY class="text-right"></ANY>
<ANY class="text-justify"></ANY>
```
## Prototype Components
### Media Object
TODO
### Number Box Object
TODO
### Balancer Object
TODO
## Basic Components
### Section
Section is served as an entry to more detailed contents. By default, each section has a white background and drop-shadow.
A section should contain a section-header and one or more section bodies.
```html
<div class="section">
<div class="section__header">
<!-- header -->
</div>
<div class="section__body">
<!-- body -->
</div>
<div class="section__body">
<!-- optionally more bodies -->
</div>
</div>
```
There will be vertical margins between section bodies.
#### Side Section
If the section is acted as a sidebar section, it should contain `side` class so that its default font size becomes smaller:
```html
<div class="section side"></div>
```
#### Title
You can use `<X class="section__title"></X>` to specify the title for a section (`X` is from `h1` to `h3`). The font size and the color of the section title is different from normal headings.
```html
<div class="section">
<div class="section__header">
<h1 class="section__title">Section title</h1>
</div>
</div>
```
#### Table
If the main content of the section is a data table (or additionally with a title and a paginator), it is suggested to apply `data-table` to the table. Table should be put inside a section-body with `no-padding` decoration.
3 * 2 table sample:
```html
<div class="section">
<!-- ... -->
<div class="section__body no-padding">
<table class="data-table">
<colgroup>
<col class="col--1">
<col class="col--2">
</colgroup>
<thead>
<tr>
<th class="col--1"></th>
<th class="col--2"></th>
</tr>
</thead>
<tbody>
<tr><td class="col--1">1,1</td><td class="col--2">1,2</td></tr>
<tr><td class="col--1">2,1</td><td class="col--2">2,2</td></tr>
<tr><td class="col--1">3,1</td><td class="col--2">3,2</td></tr>
</tbody>
</table>
</div>
</div>
```
> If you want to specify the width of a column, you should add `col--xxx` to the class name list (as shown above) and specify its width in a CSS rule like:
>
> ```css
> .col--1 { width: 120px; }
> ```
#### Samples
Section with content:
```html
<div class="section">
<div class="section__body">
Section content
</div>
</div>
```
Section with title and content:
```html
<div class="section">
<div class="section__header">
<h1 class="section__title">Section title</h1>
</div>
<div class="section__body">
Section content
</div>
</div>
```
Section with title and table content:
```html
<div class="section">
<div class="section__header">
<h1 class="section__title">Section title</h1>
</div>
<div class="section__body no-padding">
<table class="data-table">
<!-- ... -->
</table>
</div>
</div>
```
### Button
```html
<ANY class="button"></ANY>
```
Additional class names:
`rounded`: The button would have round corners.
`primary`: The button would have blue background.
`expanded`: The button would occupy full width of the parent.
`inverse`: TODO
### Input
```html
<X class="textbox"></X>
```
`X` could be `input` or `textarea`.
Additional class names:
`material`: TODO
`inverse`: TODO
### Paginator
TODO
### Menu
```html
<ol class="menu">
<li class="menu__item">
<a href="#" class="menu__link">Item</a>
</li>
<li class="menu__seperator"></li>
</ol>
```
Optionally, a menu item can be `active` or `highlight` (notice that the decorator class name is added to `menu__link` instead of `menu__item`):
```html
<li class="menu__item">
<a href="#" class="menu__link active">Active Item</a>
</li>
<li class="menu__item">
<a href="#" class="menu__link highlight">Highlight Item</a>
</li>
```
Menu can be put inside a section. It should be placed inside `section__body no-padding` or directly inside `section`:
```html
<div class="section">
<div>
<ol class="menu">
...
</ol>
</div>
</div>
```
#### Menu Item from Headings
Menu items can be automatically generated from headings. To enable this feature, you need to specify the region to extract headings and the destination of extracted menu items by setting `data-heading-extract-to` attribute:
```html
<div class="section" data-heading-extract-to="#my-menu-item">
<h1 id="h_1" data-heading>Heading 1</h1>
<h1 id="h_2" data-heading>Heading 2</h1>
</div>
```
This attribute accepts a CSS selector and all elements that contain `data-heading` attribute in the element will be extracted. `id` is essential and it will be appended to the hash part so that the page will navigate when user clicks the newly generate menu item.
The CSS selector should match a `menu__item` element. Headings will become the sub-menu of that element.
### Sticky
Sticky elements are always visible when the user scrolls. To enable sticky, add `data-sticky` attribute to the element.
Optionally, the value of `data-sticky` can be `medium` or `large`. When `data-sticky` is `medium`, the element will be sticky only when window size is larger than mobile cutoff (can be tablet or desktop). When `data-sticky` is `large`, it will take effect only when window size is larger than desktop cutoff. See section Responsive Cutoffs for actual sizes.
The sticky element is restricted to the boundary of its closest parent. You can change this behavior by assigning attribute `data-sticky-parent` to the desired parent. `data-sticky-parent` can not be used in nested sticky elements.
A sample of sticky sidebar:
```html
<div class="row" data-sticky-parent>
<div class="medium-9 columns">
<!-- main content -->
</div>
<div class="medium-3 columns">
<div data-sticky="large">
<div class="section">Sticky!</div>
</div>
</div>
</div>
```
- Because the columns will be stacked when window size is smaller than `large` (tablet and mobile) and we don't want sidebars keep sticky in such cases, we have `data-sticky="large"` so that the element will be sticky ONLY when window size is `large`.
- The height of the two columns are not equal so the sticky parent should be `.row` instead.
### CommonMark Editor
CommonMark editor can be automatically transformed from a normal textarea. Inputs in the CommonMark editor will be synced to the textarea in real-time. To enable this feature, add `data-markdown` to the `textarea` element.
Sample:
```html
<textarea name="content" class="textbox" data-markdown></textarea>
```
> To make it work with newly added textareas, you need to trigger `vjContentNew` event for the new element. Textarea with `data-markdown` should be a children of the trigger target.
### Emoji
[Emoji code](http://www.webpagefx.com/tools/emoji-cheat-sheet/) can be automatically transformed into emoji images. To enable this feature, add `data-emoji-enabled` to the element.
Sample:
```html
<div class="typo" data-emoji-enabled></div>
```
> To make it work with newly added contents, you need to trigger `vjContentNew` event for the new element.
### Dropdown
TODO
### Navigation
TODO
### Star
TODO
### Tab
TODO
### Hotkey
#### Local Hotkey
Local hotkeys are triggered when it is assigned to a parent of the active element in which user presses specific key.
To enable local hotkey on an element, add `data-hotkey` attribute:
```html
<button data-hotkey="f1"></button>
```
You can specify multiple hotkeys in the attribute.
#### Global hotkey
Global hotkeys are triggered no matter where user presses specific key.
To enable global hotkey on an element, add `data-global-hotkey` attribute:
```html
<button data-global-hotkey="f2"></button>
```
You can specify multiple hotkeys in the attribute.
#### Hotkey Syntax
`hotKeyDef[,hotKeyDef[,hotKeyDef...]]`
`hotKeyDef`: `keySequence[:event]`
`keySequence`: `key1[+key2[+key3...]]`
When `keySequence` matches, the `event` will be triggered on the element that holds the attribute. If `event` is not given in `hotKeyDef`, it will be `click` by default. In addition, if `event` is `submit`, the event will be triggered on the closest `form` element, instead of the current element.
Sample:
`ctrl+f1`: Triggers click on this element when user presses `Ctrl`+`F1` or `Command`+`F1`.
`ctrl+enter:submit`: Triggers submit on the closest `form` when user presses `Ctrl+Enter` or `Command+Enter`.
`f1:submit,f2:client`: Triggers submit when user presses `f1` and triggers click when user presses `f2`.
### Tooltip
```html
<X data-tooltip="your tooltip text"></X>
```
Optionally, you can specify a location by assigning `data-tooltip-pos` attribute. Available values are:
- `"top left"`
- `"top middle"` (default)
- `"top right"`
- `"bottom left"`
- `"bottom middle"`
- `"bottom right"`
## High-Level Components
### Comment List
TODO
## Other
### JavaScript-Responsive Visibility
#### Hide if JavaScript is disabled
```html
<ANY class="nojs--hide"></ANY>
```
#### Hide if JavaScript is enabled
```html
<ANY class="hasjs--hide"></ANY>
```
+5
View File
@@ -0,0 +1,5 @@
{
"mobile": 450,
"desktop": 1000,
"hd": 1250
}
+17
View File
@@ -0,0 +1,17 @@
aqua = #7FDBFF
blue = #0074D9
navy = #001F3F
teal = #39CCCC
green = #2ECC40
olive = #3D9970
lime = #01FF70
yellow = #FFDC00
orange = #FF851B
red = #FF4136
fuchsia = #F012BE
purple = #B10DC9
maroon = #85144B
white = #ffffff
silver = #dddddd
gray = #aaaaaa
black = #111111
+12
View File
@@ -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'
+39
View File
@@ -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);
+9
View File
@@ -0,0 +1,9 @@
word-wrap()
overflow-wrap: break-word
word-wrap: break-word
-ms-word-break: break-all
word-break: break-word
-ms-hyphens: auto
-moz-hyphens: auto
-webkit-hyphens: auto
hyphens: auto
+7
View File
@@ -0,0 +1,7 @@
rem() {
ret = ();
for arg in arguments {
push(ret, unit(arg / $font-size, 'rem'));
}
return ret;
}
+261
View File
@@ -0,0 +1,261 @@
// 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,
}
// Homework Status
$homework-status-color = {
'not_started': $text-2-color,
'ongoing': $success-color,
}
+131
View File
@@ -0,0 +1,131 @@
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,43 @@
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_url}" 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);
@@ -0,0 +1,43 @@
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_url}" 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);
@@ -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
+202
View File
@@ -0,0 +1,202 @@
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);
@@ -0,0 +1,6 @@
.menu.user-select
width: rem(200px)
.user-select__uid
font-size: rem($font-size-small)
color: $userselect-uid-color
+129
View File
@@ -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
+367
View File
@@ -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],
}));
}
}
+23
View File
@@ -0,0 +1,23 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import delay from 'vj/utils/delay';
import CmEditor from '.';
import 'vj-simplemde/src/css/simplemde.css';
import './cmeditor.styl';
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;
+91
View File
@@ -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
+66
View File
@@ -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);
+32
View File
@@ -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);
+129
View File
@@ -0,0 +1,129 @@
import _ from 'lodash';
import SimpleMDE from 'vj-simplemde';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/pascal/pascal';
import 'codemirror/mode/python/python';
import 'codemirror/mode/php/php';
import 'codemirror/mode/rust/rust';
import 'codemirror/mode/haskell/haskell';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/go/go';
import 'codemirror/mode/ruby/ruby';
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) {
const data = await request.ajax({
url: '/preview',
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();
}
}
+34
View File
@@ -0,0 +1,34 @@
.contest-type-tag
.contest-tag
font-size: rem($font-size-small)
display: inline-block
line-height: rem(22px)
padding: rem(0 10px)
vertical-align: middle
border-radius: rem(12px)
&:hover
text-decoration: none
color: #FFF !important
.contest-type-tag
&, &:visited
&, .supplementary &
color: #FFF
.icon
margin-right: rem(5px)
for key, value in $contest-color
.contest-type--{key} .contest-type-tag
background: value
&:hover
background: lighten(value, 20%)
.contest-tag
border: 1px solid $supplementary-border-color
&:hover
background: $primary-color
border-color: $primary-color
@@ -0,0 +1,21 @@
.contest-sidebar__bg
display: block
background: #67AABB url(problem-contest-bg.png) right top no-repeat
color: #FFF !important
font-size: rem($font-size)
padding: rem(20px 0)
+retina()
background-image: url(problem-contest-bg@2x.png)
background-size: 131px 150px
h1
font-size: rem($font-size-title)
color: #FFF
text-shadow: 0 1px 3px rgba(#000, 0.5)
&:hover
text-decoration: none
.contest-sidebar__status
margin-top: rem(20px)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -0,0 +1,26 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import 'pickadate/lib/themes/classic.css';
import 'pickadate/lib/themes/classic.date.css';
import 'pickadate/lib/themes/classic.time.css';
import './datepicker.styl';
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;
+54
View File
@@ -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
+145
View File
@@ -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);
+38
View File
@@ -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
+94
View File
@@ -0,0 +1,94 @@
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,
});
}
}
+113
View File
@@ -0,0 +1,113 @@
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(); // delay initialize
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);
+206
View File
@@ -0,0 +1,206 @@
import 'jquery.easing';
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+125
View File
@@ -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)
+40
View File
@@ -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%
+55
View File
@@ -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);
+8
View File
@@ -0,0 +1,8 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import Dropdown from './Dropdown';
const dropdownPage = new AutoloadPage('dropdownPage', () => {
Dropdown.registerLifeCycleHooks();
});
export default dropdownPage;
+24
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
.emoji
width: 1em
height: 1em
display: inline-block
background-size: contain
vertical-align: baseline
+21
View File
@@ -0,0 +1,21 @@
import emojify from 'emojify.js';
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+31
View File
@@ -0,0 +1,31 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+94
View File
@@ -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
+82
View File
@@ -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
+90
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+199
View File
@@ -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/PageLoader';
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;
+62
View File
@@ -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)
+29
View File
@@ -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
+20
View File
@@ -0,0 +1,20 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+72
View File
@@ -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
+14
View File
@@ -0,0 +1,14 @@
form-styles()
appearance: none
display: block
width: 100%
font-size: rem($font-size-secondary)
margin: $input-margin
height: rem($form-control-height)
line-height: 1.2
padding: rem(5px)
border: $input-border
&.inline
display: inline-block
width: auto
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+171
View File
@@ -0,0 +1,171 @@
$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
position: relative
height: rem($header-bg-height)
background: #5eafca url(./header-background.png) no-repeat center top
background-size: cover
transition: height .5s
transition-timing-function: ease-out-cubic
overflow: hidden
+retina()
background-image: url(./header-background.png)
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
display: block
width: rem($header-logo-width-real)
height: rem($header-logo-height + $header-gap-mini + $nav-item-height)
background: url(header-logo-summer.png) 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
+retina()
background-image: url(./header-logo-summer@2x.png)
+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)
background: url('../navigation/nav-logo-small_light.png') no-repeat
+retina()
background: url('../navigation/nav-logo-small@2x_light.png') no-repeat
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/PageLoader';
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
+15
View File
@@ -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'] },
];
+72
View File
@@ -0,0 +1,72 @@
/*
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
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('Code copied to clipboard!'), 1000);
});
clip.on('error', () => {
Notification.error(i18n('Copy failed :('));
});
return linkCopy;
});
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,4 @@
.homework-status--text
for key, value in $homework-status-color
&.{key}
color: value
+80
View File
@@ -0,0 +1,80 @@
import _ from 'lodash';
import { AutoloadPage } from 'vj/misc/PageLoader';
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;
+16
View File
@@ -0,0 +1,16 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import 'katex/dist/katex.min.css';
import './katex.styl';
const katexPage = new AutoloadPage('katexPage', () => {
import('katex/dist/contrib/auto-render.min.js').then((renderKatex) => {
function runKatex($containers) {
$containers.get().forEach(container => renderKatex(container));
}
runKatex($('.typo'));
$(document).on('vjContentNew', e => runKatex($(e.target).find('.typo')));
});
});
export default katexPage;
+2
View File
@@ -0,0 +1,2 @@
.katex
font-size: 1em
+29
View File
@@ -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)
+193
View File
@@ -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;
+58
View File
@@ -0,0 +1,58 @@
import _ from 'lodash';
import delay from 'vj/utils/delay';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import Marker from './Marker';
export default class MarkerReactive extends DOMAttachedObject {
static DOMAttachKey = 'vjMarkerReactiveInstance';
static initFromDOM($dom) {
return MarkerReactive.getOrConstruct($dom);
}
static initAll() {
$('[data-marker-enabled]').get().forEach(dom => MarkerReactive.initFromDOM($(dom)));
}
constructor($target) {
super($target);
this._onMouseUp = this.onMouseUp.bind(this);
this.bindEventHandlers();
}
bindEventHandlers() {
this.$dom.on('mouseup', this._onMouseUp);
}
unbindEventHandlers() {
this.$dom.off('mouseup', this._onMouseUp);
}
async onMouseUp(ev) {
await delay(1);
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
if (range.collapsed) {
return;
}
Marker.showAtPosition(this.$dom, ev.clientX, ev.clientY);
}
detach() {
if (this.detached) {
return;
}
this.unbindEventHandlers();
Marker.close();
super.detach();
}
}
_.assign(MarkerReactive, DOMAttachedObject);
+8
View File
@@ -0,0 +1,8 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import MarkerReactive from './MarkerReactive';
const markerPage = new AutoloadPage('markerPage', () => {
MarkerReactive.initAll();
});
export default markerPage;
+54
View File
@@ -0,0 +1,54 @@
[data-marker-enabled]
outline: 0
.marker
position: fixed
z-index: 100
display: none
box-shadow: 0 2px 7px rgba(#000, 0.3)
background: #FFF
font-size: rem($font-size-small)
transition: transform .1s ease-out, opacity .1s ease-out
transform: scale(0.8)
transform-origin: 0 100%
pointer-events: none
background: #FFF
padding: 1px
border: 1px solid #AAA
&.open
transform: none
pointer-events: auto
&__toolbar
padding: 1px
background: $toolbar-bg
white-space: nowrap
font-size: 0
&__action
display: inline-block
text-align: center
font-size: rem($font-size-icon)
vertical-align: top
cursor: pointer
padding: rem(8px)
color: $toolbar-fore
position: relative
&:hover
background: $toolbar-bg-hover
color: $toolbar-fore-hover
&__icon
display: block
margin: rem(2px)
width: rem(12px)
height: rem(12px)
border-radius: 50%
&.icon-yellow
background: #ffc100
&.icon-green
background: #54d651
+30
View File
@@ -0,0 +1,30 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import tpl from 'vj/utils/tpl';
const menuHeadingPage = new AutoloadPage('menuHeadingPage', null, () => {
$('[data-heading-extract-to]').get().forEach((container) => {
const $container = $(container);
const $target = $('body').find($container.attr('data-heading-extract-to'));
if ($target.length === 0) {
return;
}
let $menu = $target.children('.menu');
if ($menu.length === 0) {
$menu = $(tpl`<ul class="menu collapsed"></ul>`).appendTo($target);
$target.children('.menu__link').addClass('expandable');
}
$container.find('[data-heading]').get().forEach((heading) => {
const $heading = $(heading);
$(tpl`
<li class="menu__item">
<a class="menu__link" href="#${$heading.attr('id') || ''}">
${$heading.text()}
</a>
</li>
`).appendTo($menu);
});
});
});
export default menuHeadingPage;
+18
View File
@@ -0,0 +1,18 @@
import { AutoloadPage } from 'vj/misc/PageLoader';
import { slideDown } from 'vj/utils/slide';
import delay from 'vj/utils/delay';
function expandMenu($menu) {
slideDown($menu, 500, { opacity: 0 }, { opacity: 1 });
}
async function expandAllMenus() {
await delay(200);
$('.menu.collapsed').get().forEach(menu => expandMenu($(menu)));
}
const menuPage = new AutoloadPage('menuPage', () => {
expandAllMenus();
});
export default menuPage;
+65
View File
@@ -0,0 +1,65 @@
.menu
padding: rem(5px 0)
font-size: rem($font-size-secondary)
.icon
font-size: rem($font-size-icon)
display: inline-block
margin-right: rem(5px)
line-height: rem(18px)
vertical-align: top
.hasjs &.collapsed
display: none
.menu__item .menu
padding: 0
.menu__link
outline: 0
text-align: left
display: block
width: 100%
padding: rem(12px $section-gap-h)
line-height: rem(18px)
color: $text-1-color
border-left: 3px solid transparent
position: relative
&.highlight
background: mix($highlight-color, #FFF, 10%)
&.expandable:after
font-family: $icon-font-name
font-size: rem($font-size-icon)
content: $icon-expand_more
position: absolute
right: $section-gap-h
top: 50%
line-height: 1
margin-top: rem($font-size-icon / -2)
transition: transform .2s ease-out-cubic
opacity: 0.85
a.menu__link, button.menu__link
&.disabled
cursor: default
opacity: 0.5
&:hover
text-decoration: none
&:not(.disabled):hover
color: $text-1-color
background: $menu-item-bg-color-hover
border-color: darken($primary-color, 10%)
&.active, &.active:hover
border-color: darken($secondary-color, 30%)
background: $secondary-color
color: #FFF
.menu__item .menu .menu__link
padding-left: rem($section-gap-h + 10px)
.menu__seperator
height: 1px
border-top: 1px solid #EEE
margin: rem(5px 0)
@@ -0,0 +1,28 @@
$stripe-width = 2px
.messagepad__list-item
display: block
padding: rem(20px $section-gap-h 20px ($section-gap-h - $stripe-width))
border-left: $stripe-width solid transparent
cursor: pointer
transition: border-color .1s linear, background-color .1s linear
overflow: hidden
text-overflow: ellipsis
&:hover
border-color: #DDD
background: #F4F4F4
text-decoration: none
&.active
border-color: $secondary-color
background: rgba($secondary-color, 0.07)
.messagepad__desc
font-size: rem($font-size-small)
color: #888
max-height: 2em
margin-top: rem(5px)
overflow: hidden
text-overflow: ellipsis
word-wrap()
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function DialogueListItemComponent(props) {
const {
userName,
summary,
faceUrl,
active,
onClick,
className,
...rest
} = props;
const cn = classNames(className, 'messagepad__list-item media', {
active,
});
return (
<li {...rest}>
<a className={cn} onClick={onClick}>
<div className="media__left middle">
<img src={faceUrl} alt="avatar" width="50" height="50" className="medium user-profile-avatar" />
</div>
<div className="media__body middle">
<h3 className="messagepad__username">{userName}</h3>
<div className="messagepad__desc">{summary}</div>
</div>
</a>
</li>
);
}
DialogueListItemComponent.propTypes = {
userName: PropTypes.string.isRequired,
summary: PropTypes.string.isRequired,
faceUrl: PropTypes.string.isRequired,
active: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string,
};
@@ -0,0 +1,64 @@
$corner-size = 10px
.messagepad__message
overflow: hidden
display: flex
margin-bottom: rem(30px)
&:last-child
margin-bottom: 0
&.side--self
justify-content: flex-end
.messagepad__message__body
font-size: rem($font-size-secondary)
line-height: 1.5
padding: rem(15px)
margin-bottom: rem(8px)
position: relative
min-width: rem(100px)
white-space: pre-wrap
word-wrap()
time
opacity: 0.6
font-size: rem($font-size-small)
&:after
position: absolute
width: 0
height: 0
content: ''
top: 0
.side--other
.messagepad__message__body
margin-left: rem($corner-size + 10px)
background: $primary-color
color: #FFF
order: 1
&:after
left: rem(-($corner-size))
border-top: $corner-size solid $primary-color
border-right: 0 solid transparent
border-bottom: 0 solid transparent
border-left: $corner-size solid transparent
.side--self
.messagepad__message__body
margin-right: rem($corner-size + 10px)
background: #F0F0F0
color: #222
order: -1
time
text-align: right
&:after
right: rem(-($corner-size))
border-top: $corner-size solid #F0F0F0
border-right: $corner-size solid transparent
border-bottom: 0 solid transparent
border-left: 0 solid transparent
@@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function MessageComponent(props) {
const {
faceUrl,
isSelf,
className,
children,
...rest
} = props;
const cn = classNames(className, 'messagepad__message', {
'side--self': isSelf,
'side--other': !isSelf,
});
return (
<li {...rest} className={cn}>
<div className="messagepad__message__avatar">
<img src={faceUrl} alt="avatar" width="50" height="50" className="medium user-profile-avatar" />
</div>
<div className="messagepad__message__body">
{children}
</div>
</li>
);
}
MessageComponent.propTypes = {
isSelf: PropTypes.bool,
faceUrl: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.node,
};
@@ -0,0 +1,89 @@
$list-height = 500px
$sidebar-width = 250px
$input-height = 80px
$input-padding = 10px
$send-button-width = 50px
#messagePad
min-height: rem($list-height)
position: relative
.messagepad
position: relative
.messagepad__sidebar
border-right: 1px solid #EEE
float: left
width: rem($sidebar-width)
.messagepad__list
overflow: auto
height: rem($list-height)
.messagepad__content
position: absolute
left: rem($sidebar-width)
right: 0
top: 0
bottom: rem($input-height)
overflow: auto
padding: rem(30px)
.messagepad__input
display: none
position: absolute
left: rem($sidebar-width)
right: 0
bottom: 0
height: $input-height
border-top: 1px solid #EEE
&.visible
display: block
button
display: block
appearance: none
position: absolute
right: 0
width: rem($send-button-width)
top: 0
height: 100%
border: 0
background: none
text-align: center
line-height: rem($input-height)
font-size: rem($font-size-icon)
transition: background-color .1s, color .1s, opacity .1s
transition-timing-function: linear
outline: 0
&:enabled
cursor: pointer
background-color: $primary-color
color: #FFF
&:hover
background-color: lighten($primary-color, 10%)
&:disabled
color: $text-1-color
opacity: 0.3
.messagepad__textarea-container
position: absolute
left: rem($input-padding)
right: rem($send-button-width + $input-padding)
top: rem($input-padding)
bottom: rem($input-padding)
textarea
display: block
border: 0
background: none
padding: 0
width: 100%
height: 100%
font-size: rem($font-size-secondary)
resize: none
outline: 0
@@ -0,0 +1,81 @@
import React from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import TimeAgo from 'timeago-react';
import moment from 'moment';
import 'jquery-scroll-lock';
import 'jquery.easing';
import i18n from 'vj/utils/i18n';
import Message from './MessageComponent';
const mapStateToProps = state => ({
activeId: state.activeId,
item: state.activeId !== null
? state.dialogues[state.activeId]
: null,
});
@connect(mapStateToProps, null)
export default class MessagePadDialogueContentContainer extends React.PureComponent {
componentDidMount() {
$(this.refs.list).scrollLock({ strict: true });
}
componentWillUpdate(nextProps) {
if (nextProps.activeId !== this.props.activeId) {
this.scrollToBottom = true;
this.scrollWithAnimation = false;
return;
}
const node = this.refs.list;
if (node.scrollTop + node.offsetHeight === node.scrollHeight) {
this.scrollToBottom = true;
this.scrollWithAnimation = true;
return;
}
this.scrollToBottom = false;
}
componentDidUpdate() {
if (this.scrollToBottom) {
const node = this.refs.list;
const targetScrollTop = node.scrollHeight - node.offsetHeight;
if (this.scrollWithAnimation) {
$(node).stop().animate({ scrollTop: targetScrollTop }, 200, 'easeOutCubic');
} else {
node.scrollTop = targetScrollTop;
}
}
}
renderInner() {
if (this.props.activeId === null) {
return [];
}
return _.map(this.props.item.reply, (reply, idx) => (
<Message
key={idx}
isSelf={reply.sender_uid === UserContext.uid}
faceUrl={
reply.sender_uid === this.props.item.sender_uid
? this.props.item.sender_udoc.gravatar_url
: this.props.item.sendee_udoc.gravatar_url
}
>
<div>{reply.content}</div>
<time data-tooltip={moment(reply.at).format('YYYY-MM-DD HH:mm:ss')}>
<TimeAgo datetime={reply.at} locale={i18n('timeago_locale')} />
</time>
</Message>
));
}
render() {
return (
<ol className="messagepad__content" ref="list">
{this.renderInner()}
</ol>
);
}
}
@@ -0,0 +1,63 @@
import React from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import 'jquery-scroll-lock';
import ListItem from './DialogueListItemComponent';
const mapStateToProps = state => ({
activeId: state.activeId,
dialogues: state.dialogues,
});
const mapDispatchToProps = dispatch => ({
handleClick(id) {
dispatch({
type: 'DIALOGUES_SWITCH_TO',
payload: id,
});
},
});
@connect(mapStateToProps, mapDispatchToProps)
export default class MessagePadDialogueListContainer extends React.PureComponent {
componentDidMount() {
$(this.refs.list).scrollLock({ strict: true });
}
render() {
const orderedDialogues = _.orderBy(
_.values(this.props.dialogues),
dialogue => (dialogue.isPlaceholder
? Number.POSITIVE_INFINITY
: _.maxBy(dialogue.reply, 'at').at),
'desc',
);
return (
<ol className="messagepad__list" ref="list">
{_.map(orderedDialogues, dialogue => (
<ListItem
key={dialogue._id}
userName={
dialogue.sender_uid === UserContext.uid
? dialogue.sendee_udoc.uname
: dialogue.sender_udoc.uname
}
summary={
dialogue.isPlaceholder
? ''
: _.last(dialogue.reply).content
}
faceUrl={
dialogue.sender_uid === UserContext.uid
? dialogue.sendee_udoc.gravatar_url
: dialogue.sender_udoc.gravatar_url
}
active={dialogue._id === this.props.activeId}
onClick={() => this.props.handleClick(dialogue._id)}
/>
))}
</ol>
);
}
}
@@ -0,0 +1,135 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import Icon from 'vj/components/react/IconComponent';
import request from 'vj/utils/request';
const mapStateToProps = state => ({
activeId: state.activeId,
isPosting: state.activeId !== null
? state.isPosting[state.activeId]
: false,
inputValue: state.activeId !== null
? state.inputs[state.activeId]
: '',
});
const mapDispatchToProps = dispatch => ({
handleChange(id, value) {
if (id === null) {
return;
}
dispatch({
type: 'DIALOGUES_INPUT_CHANGED',
payload: value,
meta: {
dialogueId: id,
},
});
},
postSend(placeholderId, uid, value) {
if (placeholderId === null) {
return;
}
const req = request.post('', {
operation: 'send_message',
uid,
content: value,
});
dispatch({
type: 'DIALOGUES_POST_SEND',
payload: req,
meta: {
placeholderId,
},
});
},
postReply(id, value) {
if (id === null) {
return;
}
const req = request.post('', {
operation: 'reply_message',
message_id: id,
content: value,
});
dispatch({
type: 'DIALOGUES_POST_REPLY',
payload: req,
meta: {
dialogueId: id,
},
});
},
});
@connect(mapStateToProps, mapDispatchToProps)
export default class MessagePadInputContainer extends React.PureComponent {
static contextTypes = {
store: PropTypes.object,
};
componentWillUpdate(nextProps) {
this.focusInput = (
nextProps.activeId !== this.props.activeId
|| this.props.isPosting !== nextProps.isPosting && nextProps.isPosting === false
);
}
componentDidUpdate() {
if (this.focusInput) {
const { scrollX, scrollY } = window;
this.refs.input.focus();
window.scrollTo(scrollX, scrollY);
}
}
handleKeyDown(ev) {
if (ev.keyCode === 13 && (ev.ctrlKey || ev.metaKey)) {
this.submit();
}
}
submit() {
const state = this.context.store.getState();
if (state.dialogues[this.props.activeId].isPlaceholder) {
this.props.postSend(
this.props.activeId,
state.dialogues[this.props.activeId].sendee_uid,
this.props.inputValue,
);
} else {
this.props.postReply(
this.props.activeId,
this.props.inputValue,
);
}
}
render() {
const cn = classNames('messagepad__input', {
visible: this.props.activeId !== null,
});
return (
<div className={cn}>
<div className="messagepad__textarea-container">
<textarea
ref="input"
disabled={this.props.isPosting}
value={this.props.inputValue}
onKeyDown={ev => this.handleKeyDown(ev)}
onChange={ev => this.props.handleChange(this.props.activeId, ev.target.value)}
/>
</div>
<button
disabled={!this.props.inputValue.trim().length || this.props.isPosting}
onClick={() => this.submit()}
>
<Icon name="send" />
</button>
</div>
);
}
}
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
import Icon from 'vj/components/react/IconComponent';
import MessagePadDialogueList from './MessagePadDialogueListContainer';
import MessagePadDialogueContent from './MessagePadDialogueContentContainer';
import MessagePadInput from './MessagePadInputContainer';
const mapDispatchToProps = dispatch => ({
loadDialogues() {
dispatch({
type: 'DIALOGUES_LOAD_DIALOGUES',
payload: request.get(''),
});
},
});
@connect(null, mapDispatchToProps)
export default class MessagePadContainer extends React.PureComponent {
static propTypes = {
onAdd: PropTypes.func.isRequired,
};
componentDidMount() {
this.props.loadDialogues();
}
render() {
return (
<div className="messagepad clearfix">
<div className="messagepad__sidebar">
<div className="section__header">
<button
onClick={() => this.props.onAdd()}
className="primary rounded button"
>
<Icon name="add" />
{' '}
{i18n('New')}
</button>
</div>
<MessagePadDialogueList />
</div>
<MessagePadDialogueContent />
<MessagePadInput />
</div>
);
}
}
@@ -0,0 +1,34 @@
import 'jquery.easing';
function scrollToViewport() {
const BOUND_TOP = 60;
const BOUND_BOTTOM = 20;
const node = $('.messagepad')[0];
if (node.offsetHeight + BOUND_TOP + BOUND_BOTTOM < window.innerHeight) {
const rect = node.getBoundingClientRect();
const rectBody = document.body.getBoundingClientRect();
let targetScrollTop = null;
if (rect.top < BOUND_TOP) {
targetScrollTop = rect.top - rectBody.top - BOUND_TOP;
} else if (rect.top + node.offsetHeight > window.innerHeight) {
targetScrollTop = rect.top - rectBody.top + node.offsetHeight + BOUND_BOTTOM - window.innerHeight;
}
if (targetScrollTop !== null) {
$('html, body').stop().animate({ scrollTop: targetScrollTop }, 200, 'easeOutCubic');
}
}
}
export default function reducer(state = null, action) {
switch (action.type) {
case 'DIALOGUES_SWITCH_TO': {
scrollToViewport();
return action.payload;
}
case 'DIALOGUES_POST_SEND_FULFILLED': {
return action.payload.mdoc._id;
}
default:
return state;
}
}
@@ -0,0 +1,83 @@
import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
const { messages, udict } = action.payload;
return _.keyBy(
_.filter(
_.map(messages, m => ({
sendee_udoc: udict[m.sendee_uid],
sender_udoc: udict[m.sender_uid],
...m,
})),
m => m.sendee_udoc && m.sender_udoc,
),
'_id',
);
}
case 'DIALOGUES_CREATE': {
const { id, user } = action.payload;
return {
...state,
[id]: {
_id: id,
sender_uid: UserContext.uid,
sendee_uid: user._id,
sendee_udoc: { ...user },
reply: [],
isPlaceholder: true,
},
};
}
case 'DIALOGUES_POST_REPLY_FULFILLED': {
const id = action.meta.dialogueId;
const { reply } = action.payload;
return {
...state,
[id]: {
...state[id],
reply: [
...state[id].reply,
reply,
],
},
};
}
case 'DIALOGUES_POST_SEND_FULFILLED': {
const { placeholderId } = action.meta;
return {
..._.omit(state, placeholderId),
[action.payload.mdoc._id]: action.payload.mdoc,
};
}
case 'DIALOGUES_MESSAGE_PUSH': {
const { type, data } = action.payload;
const id = data._id;
if (type === 'new') {
return {
...state,
[id]: data,
};
} if (type === 'reply') {
if (state[id] === undefined) {
window.location.reload();
return state;
}
return {
...state,
[id]: {
...state[id],
reply: [
...state[id].reply,
data.reply[0],
],
},
};
}
return state;
}
default:
return state;
}
}
@@ -0,0 +1,14 @@
import { combineReducers } from 'redux';
import activeId from './activeId';
import dialogues from './dialogues';
import inputs from './inputs';
import isPosting from './isPosting';
const reducer = combineReducers({
activeId,
dialogues,
inputs,
isPosting,
});
export default reducer;
@@ -0,0 +1,51 @@
import _ from 'lodash';
export default function reducer(state = {}, action) {
switch (action.type) {
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
const dialogues = action.payload.messages;
return _.fromPairs(_.map(dialogues, d => [d._id, '']));
}
case 'DIALOGUES_CREATE': {
const { id } = action.payload;
return {
...state,
[id]: '',
};
}
case 'DIALOGUES_INPUT_CHANGED': {
const id = action.meta.dialogueId;
return {
...state,
[id]: action.payload,
};
}
case 'DIALOGUES_POST_REPLY_FULFILLED': {
const id = action.meta.dialogueId;
return {
...state,
[id]: '',
};
}
case 'DIALOGUES_POST_SEND_FULFILLED': {
const { placeholderId } = action.meta;
return {
..._.omit(state, placeholderId),
[action.payload.mdoc._id]: '',
};
}
case 'DIALOGUES_MESSAGE_PUSH': {
const { type, data } = action.payload;
const id = data._id;
if (type === 'new') {
return {
...state,
[id]: '',
};
}
return state;
}
default:
return state;
}
}
@@ -0,0 +1,75 @@
import _ from 'lodash';
import Notification from 'vj/components/notification';
export default function reducer(state = {}, action) {
switch (action.type) {
case 'DIALOGUES_LOAD_DIALOGUES_FULFILLED': {
const dialogues = action.payload.messages;
return _.fromPairs(_.map(dialogues, d => [d._id, false]));
}
case 'DIALOGUES_CREATE': {
const { id } = action.payload;
return {
...state,
[id]: false,
};
}
case 'DIALOGUES_POST_REPLY_PENDING': {
const id = action.meta.dialogueId;
return {
...state,
[id]: true,
};
}
case 'DIALOGUES_POST_REPLY_REJECTED': {
Notification.error(action.payload.message);
const id = action.meta.dialogueId;
return {
...state,
[id]: false,
};
}
case 'DIALOGUES_POST_REPLY_FULFILLED': {
const id = action.meta.dialogueId;
return {
...state,
[id]: false,
};
}
case 'DIALOGUES_POST_SEND_PENDING': {
const { placeholderId } = action.meta;
return {
...state,
[placeholderId]: true,
};
}
case 'DIALOGUES_POST_SEND_REJECTED': {
Notification.error(action.payload.message);
const { placeholderId } = action.meta;
return {
...state,
[placeholderId]: false,
};
}
case 'DIALOGUES_POST_SEND_FULFILLED': {
const { placeholderId } = action.meta;
return {
..._.omit(state, placeholderId),
[action.payload.mdoc._id]: false,
};
}
case 'DIALOGUES_MESSAGE_PUSH': {
const { type, data } = action.payload;
const id = data._id;
if (type === 'new') {
return {
...state,
[id]: false,
};
}
return state;
}
default:
return state;
}
}
@@ -0,0 +1,87 @@
// extracted from:
// https://github.com/jonsuh/hamburgers/blob/master/dist/hamburgers.css
// copyright @jonsuh
.hamburger
padding: 0px 0px
display: inline-block
cursor: pointer
transition-property: opacity, -webkit-filter
transition-property: opacity, filter
transition-property: opacity, filter, -webkit-filter
transition-duration: 0.15s
transition-timing-function: linear
font: inherit
color: inherit
text-transform: none
background-color: transparent
border: 0
margin: 0
overflow: visible
.hamburger:hover
opacity: 0.7
.hamburger-box
width: rem(18px)
height: rem(12px)
display: inline-block
position: relative
.hamburger-inner
display: block
top: 50%
margin-top: rem(-1px)
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after
width: rem(18px)
height: rem(2px)
background-color: #FFF
border-radius: 1px
position: absolute
transition-property: transform
transition-duration: 0.15s
transition-timing-function: ease
.layout--immersive
.hamburger-inner,
.hamburger-inner::before,
.hamburger-inner::after
background-color: #FFF
.hamburger-inner::before,
.hamburger-inner::after
content: ""
display: block
.hamburger-inner::before
top: rem(-5px)
.hamburger-inner::after
bottom: rem(-5px)
.hamburger--spin .hamburger-inner
transition-duration: 0.3s
transition-timing-function: ease-out-cubic
.hamburger--spin .hamburger-inner::before
transition: top 0.1s 0.34s ease-in, opacity 0.1s ease-in
.hamburger--spin .hamburger-inner::after
transition: bottom 0.1s 0.34s ease-in, transform 0.3s ease-in-cubic
.hamburger--spin.is-active .hamburger-inner
transform: rotate(225deg)
transition-delay: 0.14s
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1)
.hamburger--spin.is-active .hamburger-inner::before
top: 0
opacity: 0
transition: top 0.1s ease-out, opacity 0.1s 0.14s ease-out
.hamburger--spin.is-active .hamburger-inner::after
bottom: 0
transform: rotate(-90deg)
transition: bottom 0.1s ease-out, transform 0.3s 0.14s ease-out-cubic
+97
View File
@@ -0,0 +1,97 @@
import _ from 'lodash';
import responsiveCutoff from 'vj/breakpoints.json';
import { isBelow } from 'vj/utils/mediaQuery';
class MultipleStateContainer {
constructor(onStateChange, initialState = false) {
this.onStateChange = onStateChange;
this.states = {};
this.currentState = initialState;
}
set(name, value, update = true) {
this.states[name] = value;
if (update) {
this.update();
}
}
get(name) {
return this.states[name];
}
update() {
const newState = this.getState();
if (newState !== this.currentState) {
this.onStateChange(newState);
this.currentState = newState;
}
}
getState() {
return _.values(this.states).indexOf(true) > -1;
}
}
class Navigation {
constructor($nav, $navShadow) {
this.updateExpandWidth = _.throttle(this.updateExpandWidthImmediate.bind(this), 200);
this.$nav = $nav;
this.$navRow = $nav.children('.row');
this.$navShadow = $navShadow;
this.floating = new MultipleStateContainer(this.updateFloating.bind(this));
this.logoVisible = new MultipleStateContainer(this.updateLogoVisibility.bind(this));
this.expanded = new MultipleStateContainer(this.updateExpandState.bind(this));
}
updateFloating(state) {
if (state) {
this.$nav.addClass('floating');
this.$navShadow.addClass('floating');
$('body').addClass('nav--floating');
} else {
this.$nav.removeClass('floating');
this.$navShadow.removeClass('floating');
$('body').removeClass('nav--floating');
}
}
updateLogoVisibility(state) {
if (state) {
this.$nav.addClass('showlogo');
} else {
this.$nav.removeClass('showlogo');
}
}
updateExpandWidthImmediate() {
this.$navRow.css('max-width', `${window.innerWidth}px`);
}
updateExpandState(state) {
if (state) {
$(window).on('resize', this.updateExpandWidth);
this.updateExpandWidthImmediate();
} else {
$(window).off('resize', this.updateExpandWidth);
this.$navRow.css('max-width', '');
}
}
getHeight() {
if (isBelow(responsiveCutoff.mobile)) {
return 0;
}
if (this.$nav.length === 0) {
return 0;
}
if (!this._navHeight) {
this._navHeight = this.$nav.height();
}
return this._navHeight;
}
}
Navigation.instance = new Navigation($('.nav'), $('.nav--shadow'));
export default Navigation;
Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

@@ -0,0 +1,69 @@
import _ from 'lodash';
import Slideout from 'slideout';
import { AutoloadPage } from 'vj/misc/PageLoader';
import request from 'vj/utils/request';
import responsiveCutoff from 'vj/breakpoints.json';
import { isAbove } from 'vj/utils/mediaQuery';
import Navigation from '.';
const nav = Navigation.instance;
const { $nav } = nav;
function handleScroll() {
const currentState = $(window).scrollTop() > 20;
if (nav.floating.get('nonTop') !== currentState) {
nav.floating.set('nonTop', currentState);
nav.logoVisible.set('nonTop', currentState);
}
}
function handleNavLogoutClick(ev) {
const $logoutLink = $(ev.currentTarget);
request
.post($logoutLink.attr('href'))
.then(() => window.location.reload());
ev.preventDefault();
}
const navigationPage = new AutoloadPage('navigationPage', () => {
if (!document.getElementById('panel') || !document.getElementById('menu')) {
return;
}
if ($nav.length > 0
&& document.documentElement.getAttribute('data-layout') === 'basic'
&& isAbove(responsiveCutoff.mobile)
) {
$(window).on('scroll', _.throttle(handleScroll, 100));
$nav.hover(
() => nav.floating.set('hover', true),
() => nav.floating.set('hover', false),
);
$nav.on('vjDropdownShow', () => nav.floating.set('dropdown', true));
$nav.on('vjDropdownHide', () => nav.floating.set('dropdown', false));
handleScroll();
}
$(document).on('click', '[name="nav_logout"]', handleNavLogoutClick);
const slideout = new Slideout({
panel: document.getElementById('panel'),
menu: document.getElementById('menu'),
padding: 200,
tolerance: 70,
side: 'right',
});
[['beforeopen', 'add'], ['beforeclose', 'remove']].forEach(([event, action]) => {
slideout.on(event, () => $('.header__hamburger .hamburger')[`${action}Class`]('is-active'));
});
const $slideoutOverlay = $('.slideout-overlay');
$slideoutOverlay.click(() => slideout.close());
slideout.on('beforeopen', () => $slideoutOverlay.show());
slideout.on('beforeclose', () => $slideoutOverlay.hide());
$('.header__hamburger').click(() => slideout.toggle());
});
export default navigationPage;
Loaded 100 of 396 files, more files were not shown because too many files have changed in this diff. Show more