add ui
No files matched your search
@@ -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"
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"mobile": 450,
|
||||
"desktop": 1000,
|
||||
"hd": 1250
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
aqua = #7FDBFF
|
||||
blue = #0074D9
|
||||
navy = #001F3F
|
||||
teal = #39CCCC
|
||||
green = #2ECC40
|
||||
olive = #3D9970
|
||||
lime = #01FF70
|
||||
yellow = #FFDC00
|
||||
orange = #FF851B
|
||||
red = #FF4136
|
||||
fuchsia = #F012BE
|
||||
purple = #B10DC9
|
||||
maroon = #85144B
|
||||
white = #ffffff
|
||||
silver = #dddddd
|
||||
gray = #aaaaaa
|
||||
black = #111111
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file is imported in all stylus source code
|
||||
breakpoints = json('../breakpoints.json', { hash: true })
|
||||
rupture.mobile-cutoff = unit(breakpoints.mobile, 'px')
|
||||
rupture.desktop-cutoff = unit(breakpoints.desktop, 'px')
|
||||
rupture.hd-cutoff = unit(breakpoints.hd, 'px')
|
||||
|
||||
@import '~vj/misc/.iconfont/webicon.inc.styl'
|
||||
@import 'color.inc.styl'
|
||||
@import 'variables.inc.styl'
|
||||
@import 'easing.inc.styl'
|
||||
@import 'rem.inc.styl'
|
||||
@import 'functions.inc.styl'
|
||||
@@ -0,0 +1,39 @@
|
||||
// Cubic
|
||||
ease-in-cubic = cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
ease-out-cubic = cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
ease-in-out-cubic = cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
|
||||
// Circ
|
||||
ease-in-circ = cubic-bezier(0.600, 0.040, 0.980, 0.335);
|
||||
ease-out-circ = cubic-bezier(0.075, 0.820, 0.165, 1.000);
|
||||
ease-in-out-circ = cubic-bezier(0.785, 0.135, 0.150, 0.860);
|
||||
|
||||
// Expo
|
||||
ease-in-expo = cubic-bezier(0.950, 0.050, 0.795, 0.035);
|
||||
ease-out-expo = cubic-bezier(0.190, 1.000, 0.220, 1.000);
|
||||
ease-in-out-expo = cubic-bezier(1.000, 0.000, 0.000, 1.000);
|
||||
|
||||
// Quad
|
||||
ease-in-quad = cubic-bezier(0.550, 0.085, 0.680, 0.530);
|
||||
ease-out-quad = cubic-bezier(0.250, 0.460, 0.450, 0.940);
|
||||
ease-in-out-quad = cubic-bezier(0.455, 0.030, 0.515, 0.955);
|
||||
|
||||
// Quart
|
||||
ease-in-quart = cubic-bezier(0.895, 0.030, 0.685, 0.220);
|
||||
ease-out-quart = cubic-bezier(0.165, 0.840, 0.440, 1.000);
|
||||
ease-in-out-quart = cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
|
||||
// Quint
|
||||
ease-in-quint = cubic-bezier(0.755, 0.050, 0.855, 0.060);
|
||||
ease-out-quint = cubic-bezier(0.230, 1.000, 0.320, 1.000);
|
||||
ease-in-out-quint = cubic-bezier(0.860, 0.000, 0.070, 1.000);
|
||||
|
||||
// Sine
|
||||
ease-in-sine = cubic-bezier(0.470, 0.000, 0.745, 0.715);
|
||||
ease-out-sine = cubic-bezier(0.390, 0.575, 0.565, 1.000);
|
||||
ease-in-out-sine = cubic-bezier(0.445, 0.050, 0.550, 0.950);
|
||||
|
||||
// Back
|
||||
ease-in-back = cubic-bezier(0.600, -0.280, 0.735, 0.045);
|
||||
ease-out-back = cubic-bezier(0.175, 0.885, 0.320, 1.275);
|
||||
ease-in-out-back = cubic-bezier(0.680, -0.550, 0.265, 1.550);
|
||||
@@ -0,0 +1,9 @@
|
||||
word-wrap()
|
||||
overflow-wrap: break-word
|
||||
word-wrap: break-word
|
||||
-ms-word-break: break-all
|
||||
word-break: break-word
|
||||
-ms-hyphens: auto
|
||||
-moz-hyphens: auto
|
||||
-webkit-hyphens: auto
|
||||
hyphens: auto
|
||||
@@ -0,0 +1,7 @@
|
||||
rem() {
|
||||
ret = ();
|
||||
for arg in arguments {
|
||||
push(ret, unit(arg / $font-size, 'rem'));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,129 @@
|
||||
.calendar__header
|
||||
line-height: rem(45px)
|
||||
text-align: center
|
||||
|
||||
h1
|
||||
display: inline-block
|
||||
line-height: inherit
|
||||
width: rem(280px)
|
||||
|
||||
button
|
||||
font-size: rem($font-size-title)
|
||||
color: $text-3-color
|
||||
padding: 0 rem(10px)
|
||||
outline: 0
|
||||
|
||||
&:hover
|
||||
color: $primary-color
|
||||
|
||||
.calendar__week-title
|
||||
table
|
||||
width: 100%
|
||||
|
||||
th
|
||||
width: 14.28%
|
||||
padding: rem(20px 10px)
|
||||
color: $supplementary-color
|
||||
|
||||
.calendar__row
|
||||
position: relative
|
||||
|
||||
.calendar__row__bg
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
table
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
td
|
||||
width: 14.28%
|
||||
border: 1px solid $table-border-color
|
||||
|
||||
&.is-current-day
|
||||
background: lighten($highlight-color, 85%)
|
||||
|
||||
.calendar__row__content
|
||||
min-height: rem(100px)
|
||||
position: relative
|
||||
z-index: 2
|
||||
padding-bottom: rem(10px)
|
||||
|
||||
table
|
||||
width: 100%
|
||||
|
||||
th
|
||||
width: 14.28%
|
||||
padding: rem(10px)
|
||||
color: $text-2-color
|
||||
|
||||
&.is-inactive
|
||||
opacity: 0.3
|
||||
|
||||
&.is-current-day
|
||||
color: $highlight-color
|
||||
|
||||
td
|
||||
position: relative
|
||||
height: rem(24px)
|
||||
|
||||
.calendar__banner
|
||||
position: absolute
|
||||
display: block
|
||||
top: rem(2px)
|
||||
left: rem(5px)
|
||||
right: rem(5px)
|
||||
height: rem(20px)
|
||||
line-height: rem(20px)
|
||||
padding: rem(0 5px)
|
||||
font-size: rem($font-size-small)
|
||||
border-radius: rem(5px)
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
&, &:visited, &:hover, &:active
|
||||
color: #FFF
|
||||
text-decoration: none
|
||||
|
||||
&.is-trunc-begin, &.is-snap-begin
|
||||
border-top-left-radius: 0
|
||||
border-bottom-left-radius: 0
|
||||
|
||||
&.is-trunc-end, &.is-snap-end
|
||||
border-top-right-radius: 0
|
||||
border-bottom-right-radius: 0
|
||||
|
||||
&.is-snap-begin
|
||||
left: 0
|
||||
padding-left: rem(10px)
|
||||
|
||||
&.is-snap-end
|
||||
right: 0
|
||||
padding-right: rem(10px)
|
||||
|
||||
&.is-masked
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(#000, 0.1),
|
||||
rgba(#000, 0.1) 10px,
|
||||
rgba(#000, 0) 10px,
|
||||
rgba(#000, 0) 20px
|
||||
);
|
||||
|
||||
for key, value in $calendar-banner-color
|
||||
&.color-{key}
|
||||
background-color: value
|
||||
|
||||
.calendar__body-container
|
||||
position: relative
|
||||
|
||||
.calendar__body.exit
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
user-select: none
|
||||
@@ -0,0 +1,367 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
|
||||
export default class Calendar {
|
||||
constructor(events) {
|
||||
this.$dom = $(tpl`
|
||||
<div class="calendar">
|
||||
<div class="calendar__header">
|
||||
<button name="prev"><span class="icon icon-chevron_left"></span></button>
|
||||
<h1 class="calendar__header__title"></h1>
|
||||
<button name="next"><span class="icon icon-chevron_right"></span></button>
|
||||
</div>
|
||||
<div class="calendar__week-title">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>SUN</th>
|
||||
<th>MON</th>
|
||||
<th>TUE</th>
|
||||
<th>WED</th>
|
||||
<th>THU</th>
|
||||
<th>FRI</th>
|
||||
<th>SAT</th>
|
||||
</tr></thead>
|
||||
</table>
|
||||
</div>
|
||||
<div class="calendar__body-container"></div>
|
||||
</div>
|
||||
`);
|
||||
this.events = events.map(ev => ({
|
||||
...ev,
|
||||
beginAt: moment(ev.beginAt),
|
||||
endAt: moment(ev.endAt),
|
||||
maskFrom: ev.maskFrom ? moment(ev.maskFrom) : null,
|
||||
}));
|
||||
this.$dom.find('[name="prev"]').click(() => this.navToPrev());
|
||||
this.$dom.find('[name="next"]').click(() => this.navToNext());
|
||||
this.$lastBody = null;
|
||||
this.navToToday();
|
||||
}
|
||||
|
||||
getDom() {
|
||||
return this.$dom;
|
||||
}
|
||||
|
||||
navToToday() {
|
||||
if (this.animating) {
|
||||
return;
|
||||
}
|
||||
this.current = moment().date(1);
|
||||
this.update();
|
||||
}
|
||||
|
||||
navToNext() {
|
||||
if (this.animating) {
|
||||
return;
|
||||
}
|
||||
this.current.add('months', 1);
|
||||
this.update(1);
|
||||
}
|
||||
|
||||
navToPrev() {
|
||||
if (this.animating) {
|
||||
return;
|
||||
}
|
||||
this.current.subtract('months', 1);
|
||||
this.update(-1);
|
||||
}
|
||||
|
||||
update(direction = 0) {
|
||||
this.updateHeader();
|
||||
this.updateBody(direction);
|
||||
}
|
||||
|
||||
updateHeader() {
|
||||
this.$dom.find('.calendar__header__title').text(this.current.format('MMMM YYYY'));
|
||||
}
|
||||
|
||||
async updateBody(direction) {
|
||||
this.animating = true;
|
||||
const $newBody = this.buildBody();
|
||||
$newBody.appendTo(this.$dom.find('.calendar__body-container'));
|
||||
if (this.$lastBody !== null) {
|
||||
this.$lastBody
|
||||
.addClass('exit')
|
||||
.transition({
|
||||
y: direction * 100,
|
||||
opacity: 0,
|
||||
}, {
|
||||
duration: 500,
|
||||
easing: 'easeOutCubic',
|
||||
});
|
||||
await $newBody
|
||||
.css({
|
||||
y: -direction * 100,
|
||||
opacity: 0,
|
||||
})
|
||||
.transition({
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}, {
|
||||
duration: 500,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
this.$lastBody.remove();
|
||||
}
|
||||
this.$lastBody = $newBody;
|
||||
this.animating = false;
|
||||
}
|
||||
|
||||
buildBody() {
|
||||
const data = this.buildBodyData();
|
||||
const $body = $('<div class="calendar__body"></div>');
|
||||
data.forEach((week) => {
|
||||
const $row = $(tpl`<div class="calendar__row">
|
||||
<div class="calendar__row__bg"><table><tbody><tr></tr></tbody></table></div>
|
||||
<div class="calendar__row__content"><table><thead><tr></tr></thead><tbody></tbody></table></div>
|
||||
</div>`);
|
||||
const $bgContainer = $row.find('.calendar__row__bg tr');
|
||||
const $numContainer = $row.find('.calendar__row__content thead tr');
|
||||
const $bannerContainer = $row.find('.calendar__row__content tbody');
|
||||
week.days.forEach((day) => {
|
||||
const isInactive = day.active ? '' : 'is-inactive';
|
||||
const isCurrentDay = day.current ? 'is-current-day' : '';
|
||||
const today = day.current ? ' (TODAY)' : '';
|
||||
$bgContainer.append($('<td></td>').addClass(isInactive).addClass(isCurrentDay));
|
||||
$numContainer.append($(tpl`<th>${day.date.format('D')}${today}</th>`).addClass(isInactive).addClass(isCurrentDay));
|
||||
});
|
||||
week.banners.forEach((banners) => {
|
||||
const $tr = $('<tr/>');
|
||||
banners.forEach((bannerSpan) => {
|
||||
if (!bannerSpan.banner) {
|
||||
$tr.append(tpl`<td colspan="${bannerSpan.span}"></td>`);
|
||||
return;
|
||||
}
|
||||
const $cell = $(tpl`<td colspan="${bannerSpan.span}"></td>`);
|
||||
const $banner = $(tpl`
|
||||
<a
|
||||
href="${bannerSpan.banner.event.link}"
|
||||
class="calendar__banner color-${bannerSpan.banner.event.colorIndex}"
|
||||
>${bannerSpan.banner.mask ? bannerSpan.banner.event.maskTitle : bannerSpan.banner.event.title}</a>
|
||||
`);
|
||||
if (bannerSpan.banner.mask) {
|
||||
$banner.addClass('is-masked');
|
||||
}
|
||||
if (bannerSpan.banner.beginTrunc) {
|
||||
$banner.addClass('is-trunc-begin');
|
||||
} else if (bannerSpan.banner.beginSnap) {
|
||||
$banner.addClass('is-snap-begin');
|
||||
}
|
||||
if (bannerSpan.banner.endTrunc) {
|
||||
$banner.addClass('is-trunc-end');
|
||||
} else if (bannerSpan.banner.endSnap) {
|
||||
$banner.addClass('is-snap-end');
|
||||
}
|
||||
$cell.append($banner);
|
||||
$tr.append($cell);
|
||||
});
|
||||
$bannerContainer.append($tr);
|
||||
});
|
||||
$body.append($row);
|
||||
});
|
||||
return $body;
|
||||
}
|
||||
|
||||
buildBodyData() {
|
||||
const now = moment();
|
||||
const days = [];
|
||||
{
|
||||
// back fill
|
||||
const base = this.current.clone();
|
||||
const dayOfWeek = base.day();
|
||||
if (dayOfWeek > 0) {
|
||||
base.subtract(dayOfWeek + 1, 'days');
|
||||
for (let i = dayOfWeek; i > 0; --i) {
|
||||
days.push({
|
||||
active: false,
|
||||
date: base.add(1, 'days').clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
// current month
|
||||
const base = this.current.clone();
|
||||
while (base.month() === this.current.month()) {
|
||||
days.push({
|
||||
active: true,
|
||||
date: base.clone(),
|
||||
});
|
||||
base.add(1, 'days');
|
||||
}
|
||||
}
|
||||
{
|
||||
// forward fill
|
||||
const base = this.current.clone().add(1, 'months').subtract(1, 'days');
|
||||
const dayOfWeek = base.day();
|
||||
if (dayOfWeek < 6) {
|
||||
for (let i = dayOfWeek; i < 6; ++i) {
|
||||
days.push({
|
||||
active: false,
|
||||
date: base.add(1, 'days').clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
days.forEach((day) => {
|
||||
day.current = day.date.isSame(now, 'day'); // eslint-disable-line no-param-reassign
|
||||
});
|
||||
|
||||
const daysByWeek = _.chunk(days, 7);
|
||||
|
||||
const numberOfWeeks = days.length / 7;
|
||||
const bannersByWeek = _.fill(new Array(numberOfWeeks), 1).map(() => []);
|
||||
const beginDate = days[0].date.clone();
|
||||
const endDate = _.last(days).date.clone();
|
||||
|
||||
// cut events by week to banners
|
||||
this.events.forEach((ev) => {
|
||||
if (ev.endAt.isBefore(ev.beginAt, 'day')) {
|
||||
return;
|
||||
}
|
||||
if (ev.endAt.isBefore(beginDate, 'day') || ev.beginAt.isAfter(endDate, 'day')) {
|
||||
return;
|
||||
}
|
||||
if (ev.beginAt.hour() >= 22) {
|
||||
ev.beginAt.add(1, 'day').startOf('day');
|
||||
}
|
||||
if (ev.endAt.hour() <= 2) {
|
||||
ev.endAt.subtract(1, 'day').endOf('day');
|
||||
}
|
||||
let [bannerBegin, bannerBeginTruncated] = [ev.beginAt.clone(), false];
|
||||
if (bannerBegin.isBefore(beginDate, 'day')) {
|
||||
[bannerBegin, bannerBeginTruncated] = [beginDate.clone(), true];
|
||||
}
|
||||
do {
|
||||
const bannerEndMax = bannerBegin.clone().endOf('week');
|
||||
let [bannerEnd, bannerEndTruncated] = [ev.endAt.clone(), false];
|
||||
if (bannerEnd.isAfter(bannerEndMax, 'day')) {
|
||||
[bannerEnd, bannerEndTruncated] = [bannerEndMax, true];
|
||||
}
|
||||
const weekIndex = bannerBegin.clone().startOf('day').diff(beginDate.clone().startOf('day'), 'week');
|
||||
bannersByWeek[weekIndex].push({
|
||||
beginAt: bannerBegin.startOf('day'),
|
||||
beginTrunc: bannerBeginTruncated,
|
||||
endAt: bannerEnd.endOf('day'),
|
||||
endTrunc: bannerEndTruncated,
|
||||
event: ev,
|
||||
});
|
||||
if (!bannerEndTruncated) {
|
||||
break;
|
||||
}
|
||||
[bannerBegin, bannerBeginTruncated] = [bannerEnd.clone().add(1, 'day'), true];
|
||||
} while (!bannerBegin.isAfter(endDate, 'day'));
|
||||
});
|
||||
|
||||
// layout banners
|
||||
const layout = bannersByWeek
|
||||
.map(banners => _
|
||||
.sortBy(banners, [
|
||||
banner => banner.beginAt.valueOf(),
|
||||
banner => (banner.beginTrunc ? 0 : 1), // truncated events first
|
||||
banner => -banner.endAt.valueOf(), // long events first
|
||||
]))
|
||||
.map((banners) => {
|
||||
const dayBitmap = _
|
||||
.fill(new Array(7), 1)
|
||||
.map(() => []);
|
||||
banners.forEach((banner) => {
|
||||
const beginDay = banner.beginAt.day();
|
||||
const endDay = banner.endAt.day();
|
||||
// find available space
|
||||
const vIndexMax = _.max(_
|
||||
.range(beginDay, endDay + 1)
|
||||
.map(day => dayBitmap[day].length));
|
||||
let vIndex = 0;
|
||||
for (; vIndex < vIndexMax; ++vIndex) {
|
||||
if (_.every(_
|
||||
.range(beginDay, endDay + 1)
|
||||
.map(day => !dayBitmap[day][vIndex]) // eslint-disable-line no-loop-func
|
||||
)) { // eslint-disable-line function-paren-newline
|
||||
break;
|
||||
}
|
||||
}
|
||||
// fill space
|
||||
for (let i = beginDay; i <= endDay; ++i) {
|
||||
dayBitmap[i][vIndex] = banner;
|
||||
}
|
||||
});
|
||||
// merge adjacent cells and arrange banners by vertical index
|
||||
const vMaxLength = _.max(_.range(0, 7).map(day => dayBitmap[day].length));
|
||||
const weekBanners = _
|
||||
.fill(new Array(vMaxLength), 1)
|
||||
.map(() => []);
|
||||
for (let vIndex = 0; vIndex < vMaxLength; ++vIndex) {
|
||||
let last = { span: 1, banner: dayBitmap[0][vIndex] };
|
||||
weekBanners[vIndex].push(last);
|
||||
for (let day = 1; day < 7; ++day) {
|
||||
const banner = dayBitmap[day][vIndex];
|
||||
if (banner !== last.banner) {
|
||||
last = { span: 1, banner };
|
||||
weekBanners[vIndex].push(last);
|
||||
} else {
|
||||
last.span++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// cut banners by masked areas, scanning from left to right
|
||||
weekBanners.forEach((bannerSpans) => {
|
||||
for (let i = 0; i < bannerSpans.length; ++i) {
|
||||
const { banner } = bannerSpans[i];
|
||||
if (!banner) {
|
||||
continue;
|
||||
}
|
||||
if (banner.mask) {
|
||||
continue;
|
||||
}
|
||||
if (!banner.event.maskFrom) {
|
||||
continue;
|
||||
}
|
||||
if (banner.event.maskFrom.isSame(banner.event.endAt)) {
|
||||
// do not show masks if maskFrom === endAt
|
||||
continue;
|
||||
}
|
||||
if (banner.event.endAt.isSame(banner.event.beginAt, 'day')) {
|
||||
// do not show masks if endAt - beginAt <= 1 day
|
||||
continue;
|
||||
}
|
||||
if (banner.event.maskFrom.isAfter(banner.endAt, 'day')) {
|
||||
// we are not in the time region for masking
|
||||
continue;
|
||||
}
|
||||
if (banner.event.maskFrom.isSameOrBefore(banner.beginAt, 'day')) {
|
||||
// mask begins before this banner: replace current banner with masked banner
|
||||
banner.mask = true;
|
||||
} else {
|
||||
// mask begins during this banner: cut current banner into two pieces
|
||||
const newBanner = {
|
||||
...banner,
|
||||
beginAt: banner.event.maskFrom.clone(),
|
||||
beginSnap: true,
|
||||
beginTrunc: false,
|
||||
mask: true,
|
||||
};
|
||||
const newBannerSpan = {
|
||||
span: newBanner.endAt.day() - newBanner.beginAt.day() + 1,
|
||||
banner: newBanner,
|
||||
};
|
||||
banner.endAt = banner.event.maskFrom.clone().subtract(1, 'day');
|
||||
banner.endSnap = true;
|
||||
bannerSpans[i].span -= newBannerSpan.span; // eslint-disable-line no-param-reassign
|
||||
bannerSpans.splice(i + 1, 0, newBannerSpan);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
});
|
||||
return weekBanners;
|
||||
});
|
||||
return daysByWeek
|
||||
.map((daysInWeek, weekIndex) => ({
|
||||
days: daysInWeek,
|
||||
banners: layout[weekIndex],
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1,91 @@
|
||||
.simplemde
|
||||
border: 1px solid $editor-border-color
|
||||
margin: 0 0 1rem
|
||||
position: relative
|
||||
height: rem(200px)
|
||||
box-sizing: border-box
|
||||
overflow: hidden
|
||||
|
||||
.cm-s-simplemde
|
||||
border: 0
|
||||
min-height: 0
|
||||
height: auto
|
||||
padding: 0
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 40px
|
||||
width: 100%
|
||||
bottom: 0
|
||||
color: inherit
|
||||
|
||||
.simplemde-toolbar
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
border-radius: 0
|
||||
border: 0
|
||||
opacity: 0
|
||||
background: $toolbar-bg
|
||||
transform: translateY(-20px)
|
||||
transition: opacity .5s, transform .5s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:hover
|
||||
opacity: 1
|
||||
|
||||
.toolbar--visible .simplemde-toolbar
|
||||
opacity: 1
|
||||
transform: translateY(0)
|
||||
|
||||
.cm-s-simplemde .CodeMirror-scroll
|
||||
height: 100%
|
||||
min-height: 0
|
||||
font-family: $code-font-family
|
||||
color: $text-1-color
|
||||
|
||||
.simplemde-toolbar:before,
|
||||
.simplemde-toolbar:after
|
||||
display: none
|
||||
|
||||
.simplemde-toolbar
|
||||
padding: rem(5px)
|
||||
|
||||
.simplemde-toolbar i.separator
|
||||
border-right: 0
|
||||
|
||||
.simplemde-toolbar a
|
||||
color: $toolbar-fore !important
|
||||
border: 0
|
||||
background: none
|
||||
border-radius: 0
|
||||
font-size: rem($font-size-icon)
|
||||
outline: 0
|
||||
|
||||
&:hover
|
||||
background: $toolbar-bg-hover
|
||||
color: $toolbar-fore-hover !important
|
||||
border: 0
|
||||
|
||||
&.active
|
||||
background: $toolbar-bg-selected
|
||||
color: $toolbar-fore-selected !important
|
||||
border: 0
|
||||
|
||||
.simplemde-toolbar.disabled-for-preview a:not(.no-disable)
|
||||
background: none
|
||||
opacity: $toolbar-disabled-opacity
|
||||
|
||||
.cm-s-simplemde .CodeMirror-code .cm-comment
|
||||
background: none
|
||||
|
||||
.simplemde-statusbar
|
||||
display: none
|
||||
|
||||
.simplemde-preview
|
||||
background: #FFF
|
||||
|
||||
.no-heading .cm-s-simplemde .CodeMirror-code
|
||||
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4
|
||||
font-size: 1em
|
||||
line-height: 1.5
|
||||
@@ -0,0 +1,66 @@
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
export default class CmEditor extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjCmEditorInstance';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
super($dom);
|
||||
this.editor = null;
|
||||
this.options = options;
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
const { default: VjCmEditor } = await import('./vjcmeditor');
|
||||
|
||||
const hasFocus = this.$dom.is(':focus');
|
||||
const srcHeight = this.$dom.outerHeight();
|
||||
|
||||
this.editor = new VjCmEditor({
|
||||
...this.options,
|
||||
element: this.$dom.get(0),
|
||||
});
|
||||
this.moveToEnd();
|
||||
|
||||
const $editor = $(this.editor.wrapper);
|
||||
$editor.css('height', srcHeight);
|
||||
$editor.addClass('toolbar--visible');
|
||||
|
||||
if (hasFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.editor !== null);
|
||||
}
|
||||
|
||||
ensureValid() {
|
||||
if (!this.isValid()) {
|
||||
throw new Error('VjCmEditor is not loaded');
|
||||
}
|
||||
}
|
||||
|
||||
value(...argv) {
|
||||
this.ensureValid();
|
||||
const ret = this.editor.value(...argv);
|
||||
if (argv.length > 0) {
|
||||
this.moveToEnd();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.ensureValid();
|
||||
this.editor.codemirror.focus();
|
||||
}
|
||||
|
||||
moveToEnd() {
|
||||
this.ensureValid();
|
||||
const cm = this.editor.codemirror;
|
||||
cm.setCursor(cm.lineCount(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(CmEditor, DOMAttachedObject);
|
||||
@@ -0,0 +1,32 @@
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import CmEditor from '.';
|
||||
|
||||
export default class TextareaHandler extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjTextareaHandlerInstance';
|
||||
|
||||
getCmEditor() {
|
||||
return CmEditor.get(this.$dom);
|
||||
}
|
||||
|
||||
isCmEditor() {
|
||||
const editor = this.getCmEditor();
|
||||
return (editor !== undefined && editor.isValid());
|
||||
}
|
||||
|
||||
val(...argv) {
|
||||
if (this.isCmEditor()) {
|
||||
return this.getCmEditor().value(...argv);
|
||||
}
|
||||
return this.$dom.val(...argv);
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.isCmEditor()) {
|
||||
this.getCmEditor().focus();
|
||||
}
|
||||
this.$dom.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(TextareaHandler, DOMAttachedObject);
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.contest-type-tag
|
||||
.contest-tag
|
||||
font-size: rem($font-size-small)
|
||||
display: inline-block
|
||||
line-height: rem(22px)
|
||||
padding: rem(0 10px)
|
||||
vertical-align: middle
|
||||
border-radius: rem(12px)
|
||||
|
||||
&:hover
|
||||
text-decoration: none
|
||||
color: #FFF !important
|
||||
|
||||
.contest-type-tag
|
||||
&, &:visited
|
||||
&, .supplementary &
|
||||
color: #FFF
|
||||
|
||||
.icon
|
||||
margin-right: rem(5px)
|
||||
|
||||
for key, value in $contest-color
|
||||
.contest-type--{key} .contest-type-tag
|
||||
background: value
|
||||
|
||||
&:hover
|
||||
background: lighten(value, 20%)
|
||||
|
||||
.contest-tag
|
||||
border: 1px solid $supplementary-border-color
|
||||
|
||||
&:hover
|
||||
background: $primary-color
|
||||
border-color: $primary-color
|
||||
@@ -0,0 +1,21 @@
|
||||
.contest-sidebar__bg
|
||||
display: block
|
||||
background: #67AABB url(problem-contest-bg.png) right top no-repeat
|
||||
color: #FFF !important
|
||||
font-size: rem($font-size)
|
||||
padding: rem(20px 0)
|
||||
|
||||
+retina()
|
||||
background-image: url(problem-contest-bg@2x.png)
|
||||
background-size: 131px 150px
|
||||
|
||||
h1
|
||||
font-size: rem($font-size-title)
|
||||
color: #FFF
|
||||
text-shadow: 0 1px 3px rgba(#000, 0.5)
|
||||
|
||||
&:hover
|
||||
text-decoration: none
|
||||
|
||||
.contest-sidebar__status
|
||||
margin-top: rem(20px)
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1,54 @@
|
||||
.picker__holder
|
||||
margin-top: -1rem
|
||||
border-radius: 0
|
||||
transform: scale(0.9) translateZ(0)
|
||||
transform-origin: 0 0
|
||||
outline: 0
|
||||
font-size: rem($font-size-secondary)
|
||||
transition: transform .1s ease-in-out,
|
||||
opacity .1s ease-in-out,
|
||||
max-height 0s .1s,
|
||||
border-width 0s .1s
|
||||
|
||||
.picker--opened .picker__holder
|
||||
transform: scale(1) translateZ(0)
|
||||
transition: transform .1s ease-in-out,
|
||||
opacity .1s ease-in-out,
|
||||
max-height 0s,
|
||||
border-width 0s
|
||||
box-shadow: $data-picker-shadow
|
||||
|
||||
.picker__list
|
||||
padding: 0
|
||||
|
||||
.picker__list-item
|
||||
border: 0
|
||||
|
||||
.picker__list-item:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__list-item--selected, .picker__list-item--selected:hover, .picker--focused .picker__list-item--selected
|
||||
background: $primary-color
|
||||
|
||||
.picker__day--infocus:hover, .picker__day--outfocus:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__day--highlighted
|
||||
border-color: transparent
|
||||
|
||||
.picker__day--highlighted:hover, .picker--focused .picker__day--highlighted
|
||||
background: transparent
|
||||
|
||||
.picker__day--selected, .picker__day--selected:hover, .picker--focused .picker__day--selected
|
||||
background: $primary-color
|
||||
|
||||
.picker__button--today:hover, .picker__button--clear:hover, .picker__button--close:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
border-color: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__button--today:focus, .picker__button--clear:focus, .picker__button--close:focus
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
border-color: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__nav--prev:hover, .picker__nav--next:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
@@ -0,0 +1,145 @@
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
import zIndexManager from 'vj/utils/zIndexManager';
|
||||
|
||||
export default class DomDialog extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjDomDialogInstance';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
super($dom);
|
||||
this.isShown = false;
|
||||
this.isAnimating = false;
|
||||
this.options = {
|
||||
cancelByClickingBack: false,
|
||||
cancelByEsc: false,
|
||||
onDispatch: () => {},
|
||||
...options,
|
||||
};
|
||||
this._defer = null;
|
||||
}
|
||||
|
||||
async _show() {
|
||||
this.$dom.css('z-index', zIndexManager.getNext());
|
||||
this.$dom.trigger('vjDomDialogShow');
|
||||
this.isAnimating = true;
|
||||
|
||||
if (this.options.cancelByClickingBack) {
|
||||
this.$dom.on(`click.${this.eventNS}`, this.handleClick.bind(this));
|
||||
}
|
||||
if (this.options.cancelByEsc) {
|
||||
$(document).on(`keyup.${this.eventNS}`, this.handleKeyUp.bind(this));
|
||||
}
|
||||
|
||||
const $wrap = this.$dom;
|
||||
$wrap.css({
|
||||
display: 'flex',
|
||||
opacity: 0,
|
||||
});
|
||||
$wrap.width();
|
||||
$wrap.transition({
|
||||
opacity: 1,
|
||||
}, {
|
||||
duration: 100,
|
||||
easing: 'easeOutCubic',
|
||||
});
|
||||
|
||||
const $dgContent = this.$dom.find('.dialog__content');
|
||||
$dgContent.css({
|
||||
scale: 0.8,
|
||||
});
|
||||
await $dgContent
|
||||
.transition({
|
||||
scale: 1,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
this.$dom.find('[data-autofocus]').focus();
|
||||
|
||||
this.isShown = true;
|
||||
this.isAnimating = false;
|
||||
this.$dom.trigger('vjDomDialogShown');
|
||||
}
|
||||
|
||||
async _hide() {
|
||||
this.$dom.trigger('vjDomDialogHide');
|
||||
this.isAnimating = true;
|
||||
|
||||
$(document).off(`keyup.${this.eventNS}`);
|
||||
this.$dom.off(`click.${this.eventNS}`);
|
||||
|
||||
this.$dom.css({
|
||||
opacity: 1,
|
||||
});
|
||||
this.$dom.transition({
|
||||
opacity: 0,
|
||||
}, {
|
||||
duration: 200,
|
||||
});
|
||||
|
||||
const $dgContent = this.$dom.find('.dialog__content');
|
||||
$dgContent.css({
|
||||
scale: 1,
|
||||
});
|
||||
await $dgContent
|
||||
.transition({
|
||||
scale: 0.8,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
this.$dom.css('display', 'none');
|
||||
|
||||
this.isShown = false;
|
||||
this.isAnimating = false;
|
||||
this.$dom.trigger('vjDomDialogHidden');
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.isShown || this.isAnimating) {
|
||||
return Promise.reject();
|
||||
}
|
||||
this._defer = new $.Deferred();
|
||||
this._show();
|
||||
return this._defer.promise();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.isShown || this.isAnimating) {
|
||||
return false;
|
||||
}
|
||||
if (this._defer.state() === 'pending') {
|
||||
this._defer.resolve('cancel');
|
||||
}
|
||||
this._defer = null;
|
||||
this._hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatchAction(data) {
|
||||
if (this.options.onDispatch(data) === false) {
|
||||
return;
|
||||
}
|
||||
this._defer.resolve(data);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (e.target === this.$dom.get(0)) {
|
||||
this.dispatchAction('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.dispatchAction('cancel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(DomDialog, DOMAttachedObject);
|
||||
@@ -0,0 +1,38 @@
|
||||
.dialog
|
||||
position: fixed
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
opacity: 0
|
||||
z-index: 1000 // managed
|
||||
// display flex will be added after animation completes
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
display: none
|
||||
background: $dialog-layer-bg-color
|
||||
|
||||
.dialog__content
|
||||
position: relative
|
||||
|
||||
.dialog.withBg .dialog__content
|
||||
background: $dialog-bg-color
|
||||
border: 1px solid $dialog-border-color
|
||||
box-shadow: $dialog-shadow
|
||||
min-width: rem(500px)
|
||||
max-width: rem(900px)
|
||||
padding: rem(40px) rem(30px)
|
||||
|
||||
.dialog__body
|
||||
margin-bottom: rem(20px)
|
||||
min-height: rem(50px)
|
||||
|
||||
h1
|
||||
font-size: 1.6rem
|
||||
color: $dialog-title-color
|
||||
margin-bottom: 1rem
|
||||
|
||||
.dialog__action button
|
||||
margin-left: rem(10px)
|
||||
margin-bottom: 0
|
||||
@@ -0,0 +1,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -0,0 +1,125 @@
|
||||
$upvote-color = green
|
||||
$downvote-color = red
|
||||
|
||||
.dczcomments__item .is-editing
|
||||
[name="dczcomments__dummy-box"]
|
||||
display: none
|
||||
> .typo
|
||||
display: none
|
||||
|
||||
.dczcomments__box .textbox
|
||||
height: rem(180px)
|
||||
resize: vertical
|
||||
|
||||
.textbox[name="dczcomments__dummy-box"]
|
||||
height: rem(60px)
|
||||
resize: none
|
||||
font-size: rem($font-size-title)
|
||||
color: $comment-placeholder-color
|
||||
padding: rem(10px)
|
||||
|
||||
.dczcomments__item,
|
||||
.dczcomments__reply
|
||||
border-top: 1px solid $table-border-color
|
||||
|
||||
>.media
|
||||
padding: rem(20px 0)
|
||||
|
||||
&:first-child
|
||||
border-top: 0
|
||||
|
||||
.dczcomments__reply
|
||||
&:last-child
|
||||
>.media
|
||||
padding-bottom: 0
|
||||
|
||||
.dczcomments__supplementary
|
||||
float: left
|
||||
margin-bottom: rem(5px)
|
||||
|
||||
.dczcomments__operations
|
||||
font-size: rem($font-size)
|
||||
float: right
|
||||
vertical-align: top
|
||||
color: $comment-op-color
|
||||
|
||||
a, a:visited
|
||||
color: $comment-op-link-color
|
||||
margin-left: rem(3px)
|
||||
a:hover
|
||||
color: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
.view--solution .dczcomments__replies
|
||||
margin-left: rem(50px)
|
||||
|
||||
.dczcomments__item
|
||||
.vote-number
|
||||
width: rem(82px)
|
||||
text-align: center
|
||||
font-size: rem(25px)
|
||||
font-weight: 300
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
.vote-op
|
||||
position: relative
|
||||
border: 1px solid transparent
|
||||
border-radius: rem(14px)
|
||||
transition: border-color .2s linear, opacity .2s linear
|
||||
opacity: 0.5
|
||||
|
||||
&>form
|
||||
width: rem(40px)
|
||||
float: left
|
||||
position: relative
|
||||
|
||||
&:hover .vote-op
|
||||
opacity: 1
|
||||
border-color: $supplementary-border-color
|
||||
|
||||
.vote-button
|
||||
appearance: none
|
||||
border: 0
|
||||
background: none
|
||||
width: 100%
|
||||
outline: 0
|
||||
font-size: rem($font-size-icon)
|
||||
line-height: rem(26px)
|
||||
color: #AAA
|
||||
transition: color .2s linear, background-color .2s linear, box-shadow .2s linear
|
||||
|
||||
&.upvote
|
||||
border-radius: rem(13px 0 0 13px)
|
||||
padding-left: rem(7px)
|
||||
|
||||
&:hover
|
||||
background: $upvote-color
|
||||
color: #FFF
|
||||
box-shadow: 0 2px 10px rgba($upvote-color, 0.4)
|
||||
|
||||
&.active
|
||||
color: $upvote-color
|
||||
|
||||
|
||||
&.downvote
|
||||
border-radius: rem(0 13px 13px 0)
|
||||
padding-right: rem(7px)
|
||||
|
||||
&:hover
|
||||
background: $downvote-color
|
||||
color: #FFF
|
||||
box-shadow: 0 2px 10px rgba($downvote-color, 0.4)
|
||||
|
||||
&.active
|
||||
color: $downvote-color
|
||||
|
||||
&.active
|
||||
background: transparent !important
|
||||
box-shadow: none !important
|
||||
cursor: default
|
||||
|
||||
|
||||
+mobile()
|
||||
.dczcomments__item
|
||||
.media__left
|
||||
display: none
|
||||
@@ -0,0 +1,17 @@
|
||||
.discussion-node-tag
|
||||
font-size: rem($font-size-small)
|
||||
display: inline-block
|
||||
line-height: rem(22px)
|
||||
padding: rem(0 10px)
|
||||
vertical-align: middle
|
||||
border-radius: rem(12px)
|
||||
border: 1px solid $supplementary-border-color
|
||||
|
||||
&:hover
|
||||
background: $primary-color
|
||||
color: #FFF !important
|
||||
text-decoration: none
|
||||
border-color: $primary-color
|
||||
|
||||
.icon
|
||||
margin-right: rem(5px)
|
||||
@@ -0,0 +1,40 @@
|
||||
.drop-element,
|
||||
.drop-element:after,
|
||||
.drop-element:before,
|
||||
.drop-element *,
|
||||
.drop-element *:after,
|
||||
.drop-element *:before
|
||||
box-sizing: border-box
|
||||
|
||||
.drop-element
|
||||
position: absolute
|
||||
display: none
|
||||
opacity: 0
|
||||
transition: opacity .1s ease-in-out
|
||||
z-index: 1000 // managed
|
||||
|
||||
&.drop-open
|
||||
display: block
|
||||
|
||||
&.drop-open-transitionend
|
||||
display: block
|
||||
|
||||
&.drop-after-open
|
||||
opacity: 1
|
||||
|
||||
.drop-target-attached-top.drop-target-attached-left .drop-content
|
||||
transform-origin: -10% 110%
|
||||
.drop-target-attached-top.drop-target-attached-center .drop-content
|
||||
transform-origin: 50% 110%
|
||||
.drop-target-attached-top.drop-target-attached-right .drop-content
|
||||
transform-origin: 110% 110%
|
||||
.drop-target-attached-bottom.drop-target-attached-left .drop-content
|
||||
transform-origin: -10% -10%
|
||||
.drop-target-attached-bottom.drop-target-attached-center .drop-content
|
||||
transform-origin: 50% -10%
|
||||
.drop-target-attached-bottom.drop-target-attached-right .drop-content
|
||||
transform-origin: 110% -10%
|
||||
.drop-target-attached-center.drop-target-attached-right .drop-content
|
||||
transform-origin: -10% 50%
|
||||
.drop-target-attached-center.drop-target-attached-left .drop-content
|
||||
transform-origin: 110% 50%
|
||||
@@ -0,0 +1,55 @@
|
||||
import Drop from 'tether-drop';
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import responsiveCutoff from 'vj/breakpoints.json';
|
||||
|
||||
import zIndexManager from 'vj/utils/zIndexManager';
|
||||
import { isBelow } from 'vj/utils/mediaQuery';
|
||||
|
||||
export default class Dropdown extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjDropdownInstance';
|
||||
|
||||
static DOMAttachSelector = '[data-dropdown-target]';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
if ($dom.attr('data-dropdown-trigger-desktop-only') !== undefined) {
|
||||
if (isBelow(responsiveCutoff.mobile)) {
|
||||
super(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super($dom);
|
||||
this.options = {
|
||||
target: null,
|
||||
position: $dom.attr('data-dropdown-pos') || 'bottom left',
|
||||
...options,
|
||||
};
|
||||
this.dropInstance = new Drop({
|
||||
target: $dom[0],
|
||||
classes: 'dropdown',
|
||||
content: this.options.target || $.find($dom.attr('data-dropdown-target'))[0],
|
||||
position: this.options.position,
|
||||
openOn: 'hover',
|
||||
constrainToWindow: true,
|
||||
constrainToScrollParent: false,
|
||||
});
|
||||
this.dropInstance.on('open', this.onDropOpen.bind(this));
|
||||
this.dropInstance.on('close', this.onDropClose.bind(this));
|
||||
}
|
||||
|
||||
detach() {
|
||||
super.detach();
|
||||
this.dropInstance.destroy();
|
||||
}
|
||||
|
||||
onDropOpen() {
|
||||
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
|
||||
this.$dom.trigger('vjDropdownShow');
|
||||
}
|
||||
|
||||
onDropClose() {
|
||||
this.$dom.trigger('vjDropdownHide');
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(Dropdown, DOMAttachedObject);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AutoloadPage } from 'vj/misc/PageLoader';
|
||||
import Dropdown from './Dropdown';
|
||||
|
||||
const dropdownPage = new AutoloadPage('dropdownPage', () => {
|
||||
Dropdown.registerLifeCycleHooks();
|
||||
});
|
||||
|
||||
export default dropdownPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
.dropdown-target
|
||||
display: none
|
||||
|
||||
.drop .dropdown-target
|
||||
display: block
|
||||
|
||||
.dropdown
|
||||
.menu
|
||||
min-width: rem(150px)
|
||||
box-shadow: $menu-drop-shadow
|
||||
background: $menu-drop-bg-color
|
||||
font-size: rem($font-size-small)
|
||||
|
||||
.menu__link
|
||||
padding: rem(8px 10px)
|
||||
|
||||
.dropdown
|
||||
.drop-content
|
||||
transition: transform .1s ease-in-out
|
||||
transform: scale(0.9) translateZ(0)
|
||||
|
||||
&.drop-after-open
|
||||
.drop-content
|
||||
transform: scale(1) translateZ(0)
|
||||
@@ -0,0 +1,6 @@
|
||||
.emoji
|
||||
width: 1em
|
||||
height: 1em
|
||||
display: inline-block
|
||||
background-size: contain
|
||||
vertical-align: baseline
|
||||
@@ -0,0 +1,21 @@
|
||||
import emojify from 'emojify.js';
|
||||
|
||||
import { AutoloadPage } from 'vj/misc/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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,94 @@
|
||||
$footer-extra-link-gap = 20px
|
||||
|
||||
.footer
|
||||
background: $footer-bg-color
|
||||
color: $text-1-color
|
||||
|
||||
h1
|
||||
font-size: rem($font-size-title)
|
||||
|
||||
+mobile()
|
||||
padding: rem($grid-padding)
|
||||
cursor: pointer
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
.footer__category
|
||||
padding-top: rem(30px)
|
||||
padding-bottom: rem(50px)
|
||||
|
||||
+mobile()
|
||||
padding-top: rem($grid-padding)
|
||||
padding-bottom: rem($grid-padding)
|
||||
border-bottom: 1px solid darken($footer-bg-color, 5%)
|
||||
|
||||
&.expandable
|
||||
padding: 0
|
||||
|
||||
.expand-icon
|
||||
display: none
|
||||
|
||||
+mobile()
|
||||
display: inline-block
|
||||
|
||||
&.expanded .expand-icon
|
||||
transform: rotate(180deg)
|
||||
|
||||
.footer__category__list
|
||||
margin-top: rem(10px)
|
||||
margin-bottom: rem(10px)
|
||||
font-size: rem($font-size-small)
|
||||
line-height: 2
|
||||
|
||||
+mobile()
|
||||
margin-top: 0
|
||||
padding-left: rem($grid-padding)
|
||||
padding-right: rem($grid-padding)
|
||||
|
||||
.footer__category__expander
|
||||
+mobile()
|
||||
display: none
|
||||
|
||||
+above(rupture.mobile-cutoff)
|
||||
display: block !important
|
||||
|
||||
.footer__extra-left
|
||||
float: left
|
||||
|
||||
.footer__extra-right
|
||||
float: right
|
||||
|
||||
.footer__extra-link
|
||||
padding: rem(20px 0)
|
||||
font-size: rem($font-size-small)
|
||||
|
||||
.footer__extra-link-item
|
||||
float: left
|
||||
line-height: 2
|
||||
|
||||
.footer__extra-left .footer__extra-link-item
|
||||
margin-right: rem($footer-extra-link-gap)
|
||||
|
||||
.footer__extra-right .footer__extra-link-item
|
||||
margin-left: rem($footer-extra-link-gap)
|
||||
|
||||
.layout--immersive
|
||||
.footer
|
||||
background: none
|
||||
color: #FFF
|
||||
|
||||
a, a:visited, a:active
|
||||
color: #FFF
|
||||
|
||||
a:hover
|
||||
color: #FFF
|
||||
|
||||
+mobile()
|
||||
.footer__extra-left, .footer__extra-right
|
||||
float: none
|
||||
display: block
|
||||
|
||||
.footer__extra-link-item
|
||||
float: none
|
||||
display: inline-block
|
||||
margin-left: 0
|
||||
margin-right: $footer-extra-link-gap
|
||||
@@ -0,0 +1,82 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.button
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: rem($form-control-height - 2)
|
||||
background-color: lighten($default-color, 50%)
|
||||
border: 1px solid $default-color
|
||||
height: rem($form-control-height)
|
||||
padding: rem(0 25px)
|
||||
display: inline-block
|
||||
outline: 0
|
||||
margin: 0 0 1rem
|
||||
transition: background-color .2s, box-shadow .5s
|
||||
transition-timing-function: ease-out-sine
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
|
||||
&, &:visited, &:hover
|
||||
&, .typo &
|
||||
color: $text-1-color
|
||||
|
||||
&:hover, &:focus
|
||||
text-decoration: none
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover, &:focus
|
||||
background-color: darken($default-color, 5%)
|
||||
border-color: darken($default-color, 5%)
|
||||
box-shadow: 0 4px 15px rgba(darken($default-color, 10%), 0.5)
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-color
|
||||
|
||||
&.expanded
|
||||
padding-left: 0
|
||||
padding-right: 0
|
||||
text-align: center
|
||||
width: 100%
|
||||
|
||||
&.rounded
|
||||
border-radius: rem($form-control-height / 2)
|
||||
|
||||
&.primary
|
||||
border-color: $primary-color
|
||||
background-color: $primary-color
|
||||
|
||||
&, &:visited, &:hover
|
||||
&, .typo &
|
||||
color: #FFF
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover
|
||||
background-color: lighten(saturate($primary-color, 30%), 10%)
|
||||
border-color: saturate($primary-color, 70%)
|
||||
box-shadow: 0 4px 15px rgba(saturate($primary-color, 70%), 0.3)
|
||||
|
||||
&.disabled
|
||||
cursor: default
|
||||
opacity: 0.5
|
||||
|
||||
&.compact
|
||||
margin-bottom: 0
|
||||
height: rem($compact-control-height)
|
||||
line-height: rem($compact-control-height - 2)
|
||||
padding: rem(0 15px)
|
||||
|
||||
.button.inverse
|
||||
line-height: rem($form-control-height - 4)
|
||||
border: 2px solid #FFF
|
||||
background: none
|
||||
|
||||
&, &:visited
|
||||
&, .typo &
|
||||
color: #FFF
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover, &:focus
|
||||
border-color: $immersive-primary-color
|
||||
background: none
|
||||
box-shadow: none
|
||||
&, .typo &
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,90 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
$checkbox-size = 16px
|
||||
|
||||
.checkbox
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.4
|
||||
border: 0
|
||||
display: inline-block
|
||||
vertical-align: middle
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
color: lighten($text-1-color, 30%)
|
||||
margin: $input-margin
|
||||
padding: ((rem($form-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
|
||||
&:hover
|
||||
color: $text-1-color
|
||||
|
||||
input
|
||||
appearance: none
|
||||
border: 1px solid #D0D0D0
|
||||
display: inline-block
|
||||
border-radius: 1px
|
||||
vertical-align: top
|
||||
width: rem($checkbox-size)
|
||||
height: rem($checkbox-size)
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
position: relative
|
||||
outline: 0
|
||||
transition: background .1s linear
|
||||
background: none
|
||||
cursor: pointer
|
||||
|
||||
&:after
|
||||
content: ''
|
||||
width: rem(10px)
|
||||
height: rem(6px)
|
||||
position: absolute
|
||||
top: rem(3px)
|
||||
left: rem(2px)
|
||||
border: 2px solid #FFF
|
||||
border-top: none
|
||||
border-right: none
|
||||
background: transparent
|
||||
transform: rotate(-65deg) scale(0.5)
|
||||
opacity: 0
|
||||
|
||||
&:enabled:hover
|
||||
border-color: rgba($secondary-color, 0.7)
|
||||
|
||||
&:checked
|
||||
background: $secondary-color
|
||||
border-color: $secondary-color
|
||||
|
||||
&:checked:after
|
||||
opacity: 1
|
||||
transform: rotate(-45deg)
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
&.compact
|
||||
margin-bottom: 0
|
||||
line-height: 1.2
|
||||
padding: ((rem($compact-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
|
||||
input
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
|
||||
label.checkbox.inverse
|
||||
color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
input
|
||||
color: $immersive-text-color
|
||||
border-color: rgba($immersive-text-color, 0.4)
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($immersive-text-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $immersive-primary-color
|
||||
|
||||
&.focus
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AutoloadPage } from 'vj/misc/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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,62 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
$radiobox-size = 16px
|
||||
|
||||
.radiobox
|
||||
display: block
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.4
|
||||
border: 0
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
color: lighten($text-1-color, 30%)
|
||||
margin: $input-multi-line-margin
|
||||
|
||||
&:hover
|
||||
color: $text-1-color
|
||||
|
||||
input
|
||||
display: inline-block
|
||||
|
||||
.radiobox-container
|
||||
margin: $input-margin
|
||||
|
||||
// Image Radio Box
|
||||
|
||||
.radiobox.with-image
|
||||
margin: $input-image-radio-margin
|
||||
|
||||
.radiobox__image-container
|
||||
cursor: pointer
|
||||
border: 2px solid #DDD
|
||||
transition: border-color .1s linear, opacity .1s linear
|
||||
padding: 1px
|
||||
overflow: hidden
|
||||
position: relative
|
||||
opacity: 0.7
|
||||
|
||||
&:hover
|
||||
border: 2px solid $primary-color
|
||||
opacity: 1
|
||||
|
||||
&:after
|
||||
position: absolute
|
||||
width: rem(20px)
|
||||
height: rem(20px)
|
||||
right: rem(-10px)
|
||||
top: rem(-10px)
|
||||
transform: translateX(10px) translateY(-10px) rotate(45deg)
|
||||
background: $highlight-color
|
||||
content: ''
|
||||
transition: transform .1s ease-out-cubic
|
||||
|
||||
input
|
||||
visibility: hidden
|
||||
position: absolute
|
||||
|
||||
&:checked + .radiobox__image-container
|
||||
border: 2px solid $highlight-color
|
||||
opacity: 1
|
||||
|
||||
&:after
|
||||
transform: rotate(45deg)
|
||||
@@ -0,0 +1,29 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.select
|
||||
form-styles()
|
||||
padding-right: 1.1rem
|
||||
background-color: $input-background-color
|
||||
background-image: url('~vj/misc/icons/' + $icon-expand_more-file)
|
||||
background-size: 16px 16px
|
||||
background-position: right -1rem center
|
||||
background-origin: content-box
|
||||
background-repeat: no-repeat
|
||||
outline: $input-outline
|
||||
transition: outline-color .2s, border-color .2s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-border-color
|
||||
outline: $input-focus-outline
|
||||
outline-offset: 0
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
.select-container.compact .select, .select.compact
|
||||
margin-bottom: 0
|
||||
height: rem($compact-control-height)
|
||||
line-height: rem($compact-control-height - 2)
|
||||
padding-top: 0
|
||||
padding-bottom: 0
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AutoloadPage } from 'vj/misc/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;
|
||||
@@ -0,0 +1,72 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
// Normal Textbox
|
||||
input.textbox, textarea.textbox
|
||||
form-styles()
|
||||
background-color: $input-background-color
|
||||
outline: $input-outline
|
||||
transition: outline-color .2s, border-color .2s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-border-color
|
||||
outline: $input-focus-outline
|
||||
outline-offset: 0
|
||||
|
||||
// Overwrite, for IE compatibility
|
||||
&:read-only
|
||||
outline: 0
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
textarea.textbox
|
||||
height: rem(200px)
|
||||
|
||||
.data-table
|
||||
input.textbox, textarea.textbox
|
||||
margin-bottom: 0
|
||||
|
||||
// Material Textbox
|
||||
label.textbox.material
|
||||
input
|
||||
form-styles()
|
||||
border: 0
|
||||
background: none
|
||||
outline: 0
|
||||
padding-left: 0
|
||||
padding-right: 0
|
||||
color: $text-1-color
|
||||
border-bottom: 2px solid rgba($text-1-color, 0.4)
|
||||
transition: border-color .2s ease-out-cubic
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($text-1-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($text-1-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $input-focus-border-color
|
||||
|
||||
&.focus
|
||||
color: $input-focus-border-color
|
||||
|
||||
label.textbox.material.inverse
|
||||
color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
input
|
||||
color: $immersive-text-color
|
||||
border-color: rgba($immersive-text-color, 0.4)
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($immersive-text-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $immersive-primary-color
|
||||
|
||||
&.focus
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,14 @@
|
||||
form-styles()
|
||||
appearance: none
|
||||
display: block
|
||||
width: 100%
|
||||
font-size: rem($font-size-secondary)
|
||||
margin: $input-margin
|
||||
height: rem($form-control-height)
|
||||
line-height: 1.2
|
||||
padding: rem(5px)
|
||||
border: $input-border
|
||||
|
||||
&.inline
|
||||
display: inline-block
|
||||
width: auto
|
||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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
|
||||
@@ -0,0 +1,15 @@
|
||||
// The following content is extracted from:
|
||||
// https://github.com/codemirror/CodeMirror/blob/master/mode/meta.js
|
||||
|
||||
export default [
|
||||
{ name: 'C', ext: ['c', 'h'] },
|
||||
{ name: 'C++', ext: ['cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx'] },
|
||||
{ name: 'Go', ext: ['go'] },
|
||||
{ name: 'Haskell', ext: ['hs'] },
|
||||
{ name: 'Java', ext: ['java'] },
|
||||
{ name: 'JavaScript', ext: ['js'] },
|
||||
{ name: 'Pascal', ext: ['p', 'pas'] },
|
||||
{ name: 'PHP', ext: ['php', 'php3', 'php4', 'php5', 'php7', 'phtml'] },
|
||||
{ name: 'Python', ext: ['BUILD', 'bzl', 'py', 'pyw'] },
|
||||
{ name: 'Rust', ext: ['rs'] },
|
||||
];
|
||||
@@ -0,0 +1,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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
.katex
|
||||
font-size: 1em
|
||||
@@ -0,0 +1,29 @@
|
||||
.loader-container
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
top: 0
|
||||
height: 100%
|
||||
|
||||
.loader
|
||||
text-indent: -9999em
|
||||
border: rem(5px) solid rgba($primary-color, .2)
|
||||
border-left: rem(5px) solid $primary-color
|
||||
animation: load8 1.1s infinite linear
|
||||
border-radius: 50%
|
||||
width: rem(50px)
|
||||
height: rem(50px)
|
||||
margin-left: rem(-25px)
|
||||
margin-top: rem(-25px)
|
||||
left: 50%
|
||||
top: 50%
|
||||
position: absolute
|
||||
|
||||
@keyframes load8
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import _ from 'lodash';
|
||||
import delay from 'vj/utils/delay';
|
||||
import Tooltip from 'vj/components/tooltip/Tooltip';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
const MARKER_ID = `marker_${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`;
|
||||
const MARKER_OFFSET = 20;
|
||||
const MARKER_MAX_DISTANCE = 60;
|
||||
|
||||
let markerInstance = null;
|
||||
|
||||
function distanceToRect(px, py, rect) {
|
||||
const cx = (rect.left + rect.right) / 2;
|
||||
const cy = (rect.top + rect.bottom) / 2;
|
||||
const dx = Math.max(Math.abs(px - cx) - (rect.right - rect.left) / 2, 0);
|
||||
const dy = Math.max(Math.abs(py - cy) - (rect.bottom - rect.top) / 2, 0);
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
class Marker {
|
||||
static exists() {
|
||||
return markerInstance && document.getElementById(MARKER_ID);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (Marker.exists()) {
|
||||
return markerInstance;
|
||||
}
|
||||
if (markerInstance) {
|
||||
markerInstance.destroy();
|
||||
}
|
||||
this.$dom = $(`
|
||||
<div class="marker" id="${MARKER_ID}"><div class="marker__toolbar">
|
||||
<div class="marker__action" data-color="#ffff00" data-tooltip="${i18n('Mark Yellow')}"><span class="marker__icon icon-yellow"></span></div>
|
||||
<div class="marker__action" data-color="#47ff6f" data-tooltip="${i18n('Mark Green')}"><span class="marker__icon icon-green"></span></div>
|
||||
<div class="marker__action" data-color="transparent" data-tooltip="${i18n('Clear Marks')}"><span class="icon icon-erase"></span></div>
|
||||
</div></div>
|
||||
`)
|
||||
.appendTo('body');
|
||||
this.$dom.find('.marker__toolbar').on('click', '.marker__action', this.onMarkerActionClick.bind(this));
|
||||
this.$dom.on('mousedown', this.onMarkerMouseDown.bind(this));
|
||||
this.isOpen = false;
|
||||
this.bindedHandlers = false;
|
||||
this._onKeyDown = this.onKeyDown.bind(this);
|
||||
this._onScroll = _.throttle(this.onScroll.bind(this), 50);
|
||||
this._onMouseDown = this.onMouseDown.bind(this);
|
||||
this._onMouseMove = _.throttle(this.onMouseMove.bind(this), 50);
|
||||
markerInstance = this;
|
||||
}
|
||||
|
||||
markSelection(color) {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
this.$container.attr('contentEditable', true);
|
||||
if (range) {
|
||||
// recover ranges
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
if (!document.execCommand('HiliteColor', false, color)) {
|
||||
document.execCommand('BackColor', false, color);
|
||||
}
|
||||
this.$container.removeAttr('contentEditable');
|
||||
if (range) {
|
||||
// remove ranges after marking
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
showAtPosition($container, x, y) {
|
||||
this.$container = $container;
|
||||
this.$dom.css({
|
||||
display: 'block',
|
||||
opacity: 0,
|
||||
});
|
||||
const rect = this.$dom[0].getBoundingClientRect();
|
||||
this.$dom.css({
|
||||
left: x + MARKER_OFFSET,
|
||||
top: y - (rect.bottom - rect.top) - MARKER_OFFSET,
|
||||
});
|
||||
this.isOpen = true;
|
||||
this.$dom.addClass('open');
|
||||
this.updateMarkerOpacity(x, y);
|
||||
this.bindEventHandlersForClosing();
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.closeTooltipsImmediately();
|
||||
this.unbindEventHandlers();
|
||||
this.$dom
|
||||
.css({ opacity: 0 })
|
||||
.removeClass('open');
|
||||
await delay(200);
|
||||
if (!this.isOpen) {
|
||||
this.$dom.hide();
|
||||
}
|
||||
}
|
||||
|
||||
closeTooltipsImmediately() {
|
||||
const $elements = this.$dom.find('[data-tooltip]');
|
||||
$elements.get().forEach((element) => {
|
||||
const $el = $(element);
|
||||
const tooltipInstance = Tooltip.get($el);
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindEventHandlersForClosing() {
|
||||
if (this.bindedHandlers) {
|
||||
return;
|
||||
}
|
||||
$(document).on('keydown', this._onKeyDown);
|
||||
$(window).on('scroll', this._onScroll);
|
||||
$(document).on('mousedown', this._onMouseDown);
|
||||
$(document).on('mousemove', this._onMouseMove);
|
||||
this.bindedHandlers = true;
|
||||
}
|
||||
|
||||
unbindEventHandlers() {
|
||||
if (!this.bindedHandlers) {
|
||||
return;
|
||||
}
|
||||
$(document).off('keydown', this._onKeyDown);
|
||||
$(window).off('scroll', this._onScroll);
|
||||
$(document).off('mousedown', this._onMouseDown);
|
||||
$(document).off('mousemove', this._onMouseMove);
|
||||
this.bindedHandlers = false;
|
||||
}
|
||||
|
||||
onMarkerActionClick(ev) {
|
||||
const color = $(ev.currentTarget).attr('data-color');
|
||||
this.markSelection(color);
|
||||
this.close();
|
||||
}
|
||||
|
||||
onMarkerMouseDown(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onKeyDown() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
onMouseMove(ev) {
|
||||
this.updateMarkerOpacity(ev.clientX, ev.clientY);
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
updateMarkerOpacity(x, y) {
|
||||
const markerRect = this.$dom[0].getBoundingClientRect();
|
||||
const distance = distanceToRect(x, y, markerRect);
|
||||
if (distance > MARKER_MAX_DISTANCE) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
this.$dom.css('opacity', 1 - (distance / MARKER_MAX_DISTANCE));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
this.$dom.remove();
|
||||
markerInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
const MarkerInterface = {
|
||||
close() {
|
||||
if (!markerInstance) {
|
||||
return;
|
||||
}
|
||||
markerInstance.close();
|
||||
},
|
||||
showAtPosition($container, x, y) {
|
||||
const marker = new Marker();
|
||||
marker.showAtPosition($container, x, y);
|
||||
},
|
||||
};
|
||||
|
||||
export default MarkerInterface;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AutoloadPage } from 'vj/misc/PageLoader';
|
||||
import MarkerReactive from './MarkerReactive';
|
||||
|
||||
const markerPage = new AutoloadPage('markerPage', () => {
|
||||
MarkerReactive.initAll();
|
||||
});
|
||||
|
||||
export default markerPage;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
After Width: | Height: | Size: 817 B |
|
After Width: | Height: | Size: 817 B |
|
After Width: | Height: | Size: 471 B |
|
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;
|
||||