ui-default: init

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

No files matched your search

@@ -0,0 +1,125 @@
let removalChecker = null;
const res = [];
function checkResources() {
for (let i = res.length - 1; i >= 0; --i) {
if (res[i].detached || !document.body.contains(res[i].$dom[0])) {
if (!res[i].detached) res[i].detach();
res.splice(i, 1);
}
}
if (res.length === 0) {
clearInterval(removalChecker);
removalChecker = null;
}
}
function monitorResource(resource) {
res.push(resource);
if (removalChecker === null) {
removalChecker = setInterval(checkResources, 500);
}
}
export default class DOMAttachedObject {
static uniqueIdCounter = 0;
static get($obj) {
const key = this.DOMAttachKey;
return $obj.data(key);
}
static getOrConstruct($obj, ...args) {
const $singleObj = $obj.eq(0);
const key = this.DOMAttachKey;
const Protoclass = this;
const instance = this.get($singleObj);
if (instance !== undefined) return instance;
const newInstance = new Protoclass($singleObj, ...args);
// $dom may be updated in constructor so that we use $dom instead
// of $singleObj here.
if (!newInstance.$dom) return null;
newInstance.$dom.data(key, newInstance);
return newInstance;
}
static attachAll(container = document.body, ...args) {
if (process.env.NODE_ENV !== 'production') {
if (!this.DOMAttachSelector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMAttachSelector' should be specified`);
}
}
if (process.env.NODE_ENV !== 'production') {
console.time(`DOMAttachedObject[${this.DOMAttachKey}]: attachAll`);
}
$(container)
.find(this.DOMAttachSelector)
.addBack(this.DOMAttachSelector)
.get()
.forEach((element) => this.getOrConstruct($(element), ...args));
if (process.env.NODE_ENV !== 'production') {
console.timeEnd(`DOMAttachedObject[${this.DOMAttachKey}]: attachAll`);
}
}
static detachAll(container = document.body) {
const selector = this.DOMDetachSelector || this.DOMAttachSelector;
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMDetachSelector' or 'DOMAttachSelector' should be specified`);
}
}
if (process.env.NODE_ENV !== 'production') {
console.time(`DOMAttachedObject[${this.DOMAttachKey}]: detachAll`);
}
$(container)
.find(selector)
.addBack(selector)
.get()
.forEach((element) => {
const instance = this.get($(element));
if (instance) {
instance.detach();
}
});
if (process.env.NODE_ENV !== 'production') {
console.timeEnd(`DOMAttachedObject[${this.DOMAttachKey}]: detachAll`);
}
}
static registerLifeCycleHooks(attach = true) {
if (process.env.NODE_ENV !== 'production') {
if (!this.DOMAttachSelector) {
// eslint-disable-next-line quotes
throw new Error(`'DOMAttachSelector' should be specified`);
}
}
$(document).on('vjContentNew', (e) => this.attachAll(e.target));
$(document).on('vjContentRemove', (e) => this.detachAll(e.target));
if (attach) {
this.attachAll();
}
}
detach() {
if (this.constructor.DOMAttachKey) {
this.$dom.removeData(this.constructor.DOMAttachKey);
}
this.detached = true;
}
constructor($dom, monitorDetach = false) {
if ($dom == null) {
return null;
}
this.$dom = $dom;
this.id = ++DOMAttachedObject.uniqueIdCounter;
this.eventNS = `vj4obj_${this.id}`;
this.detached = false;
if (monitorDetach) {
monitorResource(this);
}
}
}
@@ -0,0 +1,44 @@
import _ from 'lodash';
import tpl from 'vj/utils/tpl';
import request from 'vj/utils/request';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
function getText(domain) {
return domain._id;
}
function getItems(val) {
return request.get('/domain/search', { q: val });
}
function renderItem(domain) {
return tpl`
<div class="media">
<div class="media__left medium">
<img class="small domain-profile-avatar" src="${domain.gravatar}" width="30" height="30">
</div>
<div class="media__body medium">
<div class="domain-select__name">${domain.name}</div>
<div class="domain-select__id">ID = ${domain._id}</div>
</div>
</div>
`;
}
export default class DomainSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'vjDomainSelectAutoCompleteInstance';
constructor($dom, options) {
super($dom, {
classes: 'domain-select',
items: getItems,
render: renderItem,
text: getText,
...options,
});
}
}
_.assign(DomainSelectAutoComplete, DOMAttachedObject);
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
@@ -0,0 +1,44 @@
import _ from 'lodash';
import tpl from 'vj/utils/tpl';
import request from 'vj/utils/request';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
function getText(user) {
return user.uname;
}
function getItems(val) {
return request.get('/user/search', { q: val });
}
function renderItem(user) {
return tpl`
<div class="media">
<div class="media__left medium">
<img class="small user-profile-avatar" src="${user.gravatar}" width="30" height="30">
</div>
<div class="media__body medium">
<div class="user-select__uname">${user.uname}</div>
<div class="user-select__uid">UID = ${user._id}</div>
</div>
</div>
`;
}
export default class UserSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'vjUserSelectAutoCompleteInstance';
constructor($dom, options) {
super($dom, {
classes: 'user-select',
items: getItems,
render: renderItem,
text: getText,
...options,
});
}
}
_.assign(UserSelectAutoComplete, DOMAttachedObject);
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
@@ -0,0 +1,11 @@
.drop-element.autocomplete
max-width: rem(250px)
.empty-row
color: $autocomplete-empty-row-color
font-size: $(font-size-small)
padding: rem(10px)
.menu
max-height: rem(250px)
overflow: auto
@@ -0,0 +1,6 @@
.menu.domain-select
width: rem(200px)
.domain-select__id
font-size: rem($font-size-small)
color: $userselect-uid-color
@@ -0,0 +1,203 @@
import Drop from 'tether-drop';
import _ from 'lodash';
import 'jquery-scroll-lock';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import zIndexManager from 'vj/utils/zIndexManager';
export default class AutoComplete extends DOMAttachedObject {
static DOMAttachKey = 'vjAutoCompleteInstance';
constructor($dom, options = {}) {
super($dom);
this.options = {
items: async () => [],
render: () => '',
text: () => null,
minChar: 1,
cache: true,
clearDefaultValue: true,
position: 'bottom left',
classes: '',
...options,
};
this.clear(this.options.clearDefaultValue);
this.menuShown = false;
this.cache = {};
this.currentItems = [];
this.$menu = $(`<ol class="menu ${this.options.classes}"></ol>`);
this.$menu.scrollLock({ strict: false });
this.$menu.on('mousedown', this.onMenuClick.bind(this));
this.$menu.on('mousedown', '.menu__item', this.onItemClick.bind(this));
this.dropInstance = new Drop({
classes: 'autocomplete dropdown',
target: this.$dom[0],
content: this.$menu[0],
position: this.options.position,
constrainToWindow: false,
constrainToScrollParent: false,
openOn: false,
});
this.dropInstance.on('open', this.onDropOpen.bind(this));
this.attach();
}
clear(clearValue = true) {
if (clearValue) {
this.$dom.val('');
}
this._value = null;
this.lastText = null;
}
attach() {
this.$dom.on(`click.${this.eventNS}`, this.onClick.bind(this));
this.$dom.on(`focus.${this.eventNS}`, this.onFocus.bind(this));
this.$dom.on(`blur.${this.eventNS}`, this.onBlur.bind(this));
this.$dom.on(`keydown.${this.eventNS}`, this.onKeyDown.bind(this));
this.$dom.on(`keyup.${this.eventNS}`, this.onKeyUp.bind(this));
}
onClick() {
this.updateOpenState();
}
onFocus() {
this.isFocus = true;
this.updateOpenState();
}
onBlur() {
this.isFocus = false;
this.updateOpenState();
}
onKeyDown() {
// TODO: Implement keyboard navigation
}
onKeyUp(ev) {
if (ev.which === 27) {
// ESC
this.close();
return;
}
if (this.$dom.val() === this.lastText) {
return;
}
this.lastText = this.$dom.val();
this.updateOpenState();
if (this.isOpen) {
this.renderList();
}
}
onMenuClick(ev) {
ev.preventDefault(); // prevent from losing focus
}
onItemClick(ev) {
const idx = $(ev.currentTarget).attr('data-idx');
if (idx === undefined) {
return;
}
const item = this.currentItems[idx];
const text = this.options.text(item);
this._value = item;
this.$dom.trigger('vjAutoCompleteSelect', item);
if (text != null) {
this.$dom.val(text);
}
this.close();
}
onDropOpen() {
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
}
async getItems(val) {
if (this.cache[val] !== undefined) {
return this.cache[val];
}
const data = await this.options.items(val);
if (this.options.cache) {
this.cache[val] = data;
}
return data;
}
getHtml(items) {
if (items.length === 0) {
return tpl`<div class="empty-row">${i18n('Oops, there are no results.')}</div>`;
}
return items.map((item, idx) => `
<li class="menu__item" data-idx="${idx}"><a href="javascript:;" class="menu__link">
${this.options.render(item)}
</a></li>
`).join('\n');
}
async renderList() {
const val = this.$dom.val();
const items = await this.getItems(val);
const html = this.getHtml(items);
this.currentItems = items;
this.$menu.html(html);
}
open() {
if (this.isOpen) {
return;
}
this.dropInstance.open();
this.isOpen = true;
}
close() {
if (!this.isOpen) {
return;
}
this.dropInstance.close();
this.isOpen = false;
}
updateOpenState() {
if (this.isFocus && this.$dom.val().length >= this.options.minChar) {
if (!this.isOpen) {
this.open();
this.renderList();
}
} else {
if (this.isOpen) {
this.close();
}
}
}
detach() {
if (this.detached) {
return;
}
super.detach();
this.$dom.off(`focus.${this.eventNS}`);
this.$dom.off(`blur.${this.eventNS}`);
this.$dom.off(`keydown.${this.eventNS}`);
this.$dom.off(`keyup.${this.eventNS}`);
this.dropInstance.destroy();
this.$menu.remove();
}
value() {
return this._value;
}
focus() {
this.$dom.focus();
}
}
_.assign(AutoComplete, DOMAttachedObject);
window.Hydro.components.autocomplete = AutoComplete;
@@ -0,0 +1,6 @@
.menu.user-select
width: rem(200px)
.user-select__uid
font-size: rem($font-size-small)
color: $userselect-uid-color
@@ -0,0 +1,129 @@
.calendar__header
line-height: rem(45px)
text-align: center
h1
display: inline-block
line-height: inherit
width: rem(280px)
button
font-size: rem($font-size-title)
color: $text-3-color
padding: 0 rem(10px)
outline: 0
&:hover
color: $primary-color
.calendar__week-title
table
width: 100%
th
width: 14.28%
padding: rem(20px 10px)
color: $supplementary-color
.calendar__row
position: relative
.calendar__row__bg
position: absolute
left: 0
top: 0
width: 100%
height: 100%
table
width: 100%
height: 100%
td
width: 14.28%
border: 1px solid $table-border-color
&.is-current-day
background: lighten($highlight-color, 85%)
.calendar__row__content
min-height: rem(100px)
position: relative
z-index: 2
padding-bottom: rem(10px)
table
width: 100%
th
width: 14.28%
padding: rem(10px)
color: $text-2-color
&.is-inactive
opacity: 0.3
&.is-current-day
color: $highlight-color
td
position: relative
height: rem(24px)
.calendar__banner
position: absolute
display: block
top: rem(2px)
left: rem(5px)
right: rem(5px)
height: rem(20px)
line-height: rem(20px)
padding: rem(0 5px)
font-size: rem($font-size-small)
border-radius: rem(5px)
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
&, &:visited, &:hover, &:active
color: #FFF
text-decoration: none
&.is-trunc-begin, &.is-snap-begin
border-top-left-radius: 0
border-bottom-left-radius: 0
&.is-trunc-end, &.is-snap-end
border-top-right-radius: 0
border-bottom-right-radius: 0
&.is-snap-begin
left: 0
padding-left: rem(10px)
&.is-snap-end
right: 0
padding-right: rem(10px)
&.is-masked
background-image: repeating-linear-gradient(
45deg,
rgba(#000, 0.1),
rgba(#000, 0.1) 10px,
rgba(#000, 0) 10px,
rgba(#000, 0) 20px
);
for key, value in $calendar-banner-color
&.color-{key}
background-color: value
.calendar__body-container
position: relative
.calendar__body.exit
position: absolute
left: 0
top: 0
width: 100%
user-select: none
@@ -0,0 +1,367 @@
import _ from 'lodash';
import moment from 'moment';
import tpl from 'vj/utils/tpl';
export default class Calendar {
constructor(events) {
this.$dom = $(tpl`
<div class="calendar">
<div class="calendar__header">
<button name="prev"><span class="icon icon-chevron_left"></span></button>
<h1 class="calendar__header__title"></h1>
<button name="next"><span class="icon icon-chevron_right"></span></button>
</div>
<div class="calendar__week-title">
<table>
<thead><tr>
<th>SUN</th>
<th>MON</th>
<th>TUE</th>
<th>WED</th>
<th>THU</th>
<th>FRI</th>
<th>SAT</th>
</tr></thead>
</table>
</div>
<div class="calendar__body-container"></div>
</div>
`);
this.events = events.map((ev) => ({
...ev,
beginAt: moment(ev.beginAt),
endAt: moment(ev.endAt),
maskFrom: ev.maskFrom ? moment(ev.maskFrom) : null,
}));
this.$dom.find('[name="prev"]').click(() => this.navToPrev());
this.$dom.find('[name="next"]').click(() => this.navToNext());
this.$lastBody = null;
this.navToToday();
}
getDom() {
return this.$dom;
}
navToToday() {
if (this.animating) {
return;
}
this.current = moment().date(1);
this.update();
}
navToNext() {
if (this.animating) {
return;
}
this.current.add('months', 1);
this.update(1);
}
navToPrev() {
if (this.animating) {
return;
}
this.current.subtract('months', 1);
this.update(-1);
}
update(direction = 0) {
this.updateHeader();
this.updateBody(direction);
}
updateHeader() {
this.$dom.find('.calendar__header__title').text(this.current.format('MMMM YYYY'));
}
async updateBody(direction) {
this.animating = true;
const $newBody = this.buildBody();
$newBody.appendTo(this.$dom.find('.calendar__body-container'));
if (this.$lastBody !== null) {
this.$lastBody
.addClass('exit')
.transition({
y: direction * 100,
opacity: 0,
}, {
duration: 500,
easing: 'easeOutCubic',
});
await $newBody
.css({
y: -direction * 100,
opacity: 0,
})
.transition({
y: 0,
opacity: 1,
}, {
duration: 500,
easing: 'easeOutCubic',
})
.promise();
this.$lastBody.remove();
}
this.$lastBody = $newBody;
this.animating = false;
}
buildBody() {
const data = this.buildBodyData();
const $body = $('<div class="calendar__body"></div>');
data.forEach((week) => {
const $row = $(tpl`<div class="calendar__row">
<div class="calendar__row__bg"><table><tbody><tr></tr></tbody></table></div>
<div class="calendar__row__content"><table><thead><tr></tr></thead><tbody></tbody></table></div>
</div>`);
const $bgContainer = $row.find('.calendar__row__bg tr');
const $numContainer = $row.find('.calendar__row__content thead tr');
const $bannerContainer = $row.find('.calendar__row__content tbody');
week.days.forEach((day) => {
const isInactive = day.active ? '' : 'is-inactive';
const isCurrentDay = day.current ? 'is-current-day' : '';
const today = day.current ? ' (TODAY)' : '';
$bgContainer.append($('<td></td>').addClass(isInactive).addClass(isCurrentDay));
$numContainer.append($(tpl`<th>${day.date.format('D')}${today}</th>`).addClass(isInactive).addClass(isCurrentDay));
});
week.banners.forEach((banners) => {
const $tr = $('<tr/>');
banners.forEach((bannerSpan) => {
if (!bannerSpan.banner) {
$tr.append(tpl`<td colspan="${bannerSpan.span}"></td>`);
return;
}
const $cell = $(tpl`<td colspan="${bannerSpan.span}"></td>`);
const $banner = $(tpl`
<a
href="${bannerSpan.banner.event.link}"
class="calendar__banner color-${bannerSpan.banner.event.colorIndex}"
>${bannerSpan.banner.mask ? bannerSpan.banner.event.maskTitle : bannerSpan.banner.event.title}</a>
`);
if (bannerSpan.banner.mask) {
$banner.addClass('is-masked');
}
if (bannerSpan.banner.beginTrunc) {
$banner.addClass('is-trunc-begin');
} else if (bannerSpan.banner.beginSnap) {
$banner.addClass('is-snap-begin');
}
if (bannerSpan.banner.endTrunc) {
$banner.addClass('is-trunc-end');
} else if (bannerSpan.banner.endSnap) {
$banner.addClass('is-snap-end');
}
$cell.append($banner);
$tr.append($cell);
});
$bannerContainer.append($tr);
});
$body.append($row);
});
return $body;
}
buildBodyData() {
const now = moment();
const days = [];
{
// back fill
const base = this.current.clone();
const dayOfWeek = base.day();
if (dayOfWeek > 0) {
base.subtract(dayOfWeek + 1, 'days');
for (let i = dayOfWeek; i > 0; --i) {
days.push({
active: false,
date: base.add(1, 'days').clone(),
});
}
}
}
{
// current month
const base = this.current.clone();
while (base.month() === this.current.month()) {
days.push({
active: true,
date: base.clone(),
});
base.add(1, 'days');
}
}
{
// forward fill
const base = this.current.clone().add(1, 'months').subtract(1, 'days');
const dayOfWeek = base.day();
if (dayOfWeek < 6) {
for (let i = dayOfWeek; i < 6; ++i) {
days.push({
active: false,
date: base.add(1, 'days').clone(),
});
}
}
}
days.forEach((day) => {
day.current = day.date.isSame(now, 'day'); // eslint-disable-line no-param-reassign
});
const daysByWeek = _.chunk(days, 7);
const numberOfWeeks = days.length / 7;
const bannersByWeek = _.fill(new Array(numberOfWeeks), 1).map(() => []);
const beginDate = days[0].date.clone();
const endDate = _.last(days).date.clone();
// cut events by week to banners
this.events.forEach((ev) => {
if (ev.endAt.isBefore(ev.beginAt, 'day')) {
return;
}
if (ev.endAt.isBefore(beginDate, 'day') || ev.beginAt.isAfter(endDate, 'day')) {
return;
}
if (ev.beginAt.hour() >= 22) {
ev.beginAt.add(1, 'day').startOf('day');
}
if (ev.endAt.hour() <= 2) {
ev.endAt.subtract(1, 'day').endOf('day');
}
let [bannerBegin, bannerBeginTruncated] = [ev.beginAt.clone(), false];
if (bannerBegin.isBefore(beginDate, 'day')) {
[bannerBegin, bannerBeginTruncated] = [beginDate.clone(), true];
}
do {
const bannerEndMax = bannerBegin.clone().endOf('week');
let [bannerEnd, bannerEndTruncated] = [ev.endAt.clone(), false];
if (bannerEnd.isAfter(bannerEndMax, 'day')) {
[bannerEnd, bannerEndTruncated] = [bannerEndMax, true];
}
const weekIndex = bannerBegin.clone().startOf('day').diff(beginDate.clone().startOf('day'), 'week');
bannersByWeek[weekIndex].push({
beginAt: bannerBegin.startOf('day'),
beginTrunc: bannerBeginTruncated,
endAt: bannerEnd.endOf('day'),
endTrunc: bannerEndTruncated,
event: ev,
});
if (!bannerEndTruncated) {
break;
}
[bannerBegin, bannerBeginTruncated] = [bannerEnd.clone().add(1, 'day'), true];
} while (!bannerBegin.isAfter(endDate, 'day'));
});
// layout banners
const layout = bannersByWeek
.map((banners) => _
.sortBy(banners, [
(banner) => banner.beginAt.valueOf(),
(banner) => (banner.beginTrunc ? 0 : 1), // truncated events first
(banner) => -banner.endAt.valueOf(), // long events first
]))
.map((banners) => {
const dayBitmap = _
.fill(new Array(7), 1)
.map(() => []);
banners.forEach((banner) => {
const beginDay = banner.beginAt.day();
const endDay = banner.endAt.day();
// find available space
const vIndexMax = _.max(_
.range(beginDay, endDay + 1)
.map((day) => dayBitmap[day].length));
let vIndex = 0;
for (; vIndex < vIndexMax; ++vIndex) {
if (_.every(_
.range(beginDay, endDay + 1)
.map((day) => !dayBitmap[day][vIndex]) // eslint-disable-line no-loop-func
)) { // eslint-disable-line function-paren-newline
break;
}
}
// fill space
for (let i = beginDay; i <= endDay; ++i) {
dayBitmap[i][vIndex] = banner;
}
});
// merge adjacent cells and arrange banners by vertical index
const vMaxLength = _.max(_.range(0, 7).map((day) => dayBitmap[day].length));
const weekBanners = _
.fill(new Array(vMaxLength), 1)
.map(() => []);
for (let vIndex = 0; vIndex < vMaxLength; ++vIndex) {
let last = { span: 1, banner: dayBitmap[0][vIndex] };
weekBanners[vIndex].push(last);
for (let day = 1; day < 7; ++day) {
const banner = dayBitmap[day][vIndex];
if (banner !== last.banner) {
last = { span: 1, banner };
weekBanners[vIndex].push(last);
} else {
last.span++;
}
}
}
// cut banners by masked areas, scanning from left to right
weekBanners.forEach((bannerSpans) => {
for (let i = 0; i < bannerSpans.length; ++i) {
const { banner } = bannerSpans[i];
if (!banner) {
continue;
}
if (banner.mask) {
continue;
}
if (!banner.event.maskFrom) {
continue;
}
if (banner.event.maskFrom.isSame(banner.event.endAt)) {
// do not show masks if maskFrom === endAt
continue;
}
if (banner.event.endAt.isSame(banner.event.beginAt, 'day')) {
// do not show masks if endAt - beginAt <= 1 day
continue;
}
if (banner.event.maskFrom.isAfter(banner.endAt, 'day')) {
// we are not in the time region for masking
continue;
}
if (banner.event.maskFrom.isSameOrBefore(banner.beginAt, 'day')) {
// mask begins before this banner: replace current banner with masked banner
banner.mask = true;
} else {
// mask begins during this banner: cut current banner into two pieces
const newBanner = {
...banner,
beginAt: banner.event.maskFrom.clone(),
beginSnap: true,
beginTrunc: false,
mask: true,
};
const newBannerSpan = {
span: newBanner.endAt.day() - newBanner.beginAt.day() + 1,
banner: newBanner,
};
banner.endAt = banner.event.maskFrom.clone().subtract(1, 'day');
banner.endSnap = true;
bannerSpans[i].span -= newBannerSpan.span; // eslint-disable-line no-param-reassign
bannerSpans.splice(i + 1, 0, newBannerSpan);
i++;
}
}
});
return weekBanners;
});
return daysByWeek
.map((daysInWeek, weekIndex) => ({
days: daysInWeek,
banners: layout[weekIndex],
}));
}
}
@@ -0,0 +1,20 @@
import { AutoloadPage } from 'vj/misc/Page';
import delay from 'vj/utils/delay';
import CmEditor from '.';
function runSubstitute($container) {
const selector = ['textarea[data-markdown]'];
$container.find(selector.join(', ')).get().forEach((element) => {
CmEditor.getOrConstruct($(element));
});
}
const cmEditorPage = new AutoloadPage('cmEditorPage', () => {
runSubstitute($('body'));
$(document).on('vjContentNew', async (e) => {
await delay(0);
runSubstitute($(e.target));
});
});
export default cmEditorPage;
@@ -0,0 +1,91 @@
.simplemde
border: 1px solid $editor-border-color
margin: 0 0 1rem
position: relative
height: rem(200px)
box-sizing: border-box
overflow: hidden
.cm-s-simplemde
border: 0
min-height: 0
height: auto
padding: 0
position: absolute
left: 0
top: 40px
width: 100%
bottom: 0
color: inherit
.simplemde-toolbar
position: absolute
left: 0
top: 0
width: 100%
border-radius: 0
border: 0
opacity: 0
background: $toolbar-bg
transform: translateY(-20px)
transition: opacity .5s, transform .5s
transition-timing-function: ease-out-cubic
&:hover
opacity: 1
.toolbar--visible .simplemde-toolbar
opacity: 1
transform: translateY(0)
.cm-s-simplemde .CodeMirror-scroll
height: 100%
min-height: 0
font-family: $code-font-family
color: $text-1-color
.simplemde-toolbar:before,
.simplemde-toolbar:after
display: none
.simplemde-toolbar
padding: rem(5px)
.simplemde-toolbar i.separator
border-right: 0
.simplemde-toolbar a
color: $toolbar-fore !important
border: 0
background: none
border-radius: 0
font-size: rem($font-size-icon)
outline: 0
&:hover
background: $toolbar-bg-hover
color: $toolbar-fore-hover !important
border: 0
&.active
background: $toolbar-bg-selected
color: $toolbar-fore-selected !important
border: 0
.simplemde-toolbar.disabled-for-preview a:not(.no-disable)
background: none
opacity: $toolbar-disabled-opacity
.cm-s-simplemde .CodeMirror-code .cm-comment
background: none
.simplemde-statusbar
display: none
.simplemde-preview
background: #FFF
.no-heading .cm-s-simplemde .CodeMirror-code
.cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4
font-size: 1em
line-height: 1.5
@@ -0,0 +1,66 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
export default class CmEditor extends DOMAttachedObject {
static DOMAttachKey = 'vjCmEditorInstance';
constructor($dom, options = {}) {
super($dom);
this.editor = null;
this.options = options;
this.init();
}
async init() {
const { default: VjCmEditor } = await import('./vjcmeditor');
const hasFocus = this.$dom.is(':focus');
const srcHeight = this.$dom.outerHeight();
this.editor = new VjCmEditor({
...this.options,
element: this.$dom.get(0),
});
this.moveToEnd();
const $editor = $(this.editor.wrapper);
$editor.css('height', srcHeight);
$editor.addClass('toolbar--visible');
if (hasFocus) {
this.focus();
}
}
isValid() {
return (this.editor !== null);
}
ensureValid() {
if (!this.isValid()) {
throw new Error('VjCmEditor is not loaded');
}
}
value(...argv) {
this.ensureValid();
const ret = this.editor.value(...argv);
if (argv.length > 0) {
this.moveToEnd();
}
return ret;
}
focus() {
this.ensureValid();
this.editor.codemirror.focus();
}
moveToEnd() {
this.ensureValid();
const cm = this.editor.codemirror;
cm.setCursor(cm.lineCount(), 0);
}
}
_.assign(CmEditor, DOMAttachedObject);
@@ -0,0 +1,32 @@
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import CmEditor from '.';
export default class TextareaHandler extends DOMAttachedObject {
static DOMAttachKey = 'vjTextareaHandlerInstance';
getCmEditor() {
return CmEditor.get(this.$dom);
}
isCmEditor() {
const editor = this.getCmEditor();
return (editor !== undefined && editor.isValid());
}
val(...argv) {
if (this.isCmEditor()) {
return this.getCmEditor().value(...argv);
}
return this.$dom.val(...argv);
}
focus() {
if (this.isCmEditor()) {
this.getCmEditor().focus();
}
this.$dom.focus();
}
}
_.assign(TextareaHandler, DOMAttachedObject);
@@ -0,0 +1,120 @@
import _ from 'lodash';
import SimpleMDE from 'vj-simplemde';
import request from 'vj/utils/request';
import i18n from 'vj/utils/i18n';
export default class VjCmEditor extends SimpleMDE {
constructor(options = {}) {
super({
autoDownloadFontAwesome: false,
spellChecker: false,
forceSync: true,
toolbar: [
{
name: 'bold',
action: SimpleMDE.toggleBold,
className: 'icon icon-bold',
title: i18n('Bold'),
},
{
name: 'italic',
action: SimpleMDE.toggleItalic,
className: 'icon icon-italic',
title: i18n('Italic'),
},
'|',
{
name: 'quote',
action: SimpleMDE.toggleBlockquote,
className: 'icon icon-quote',
title: i18n('Quote'),
},
{
name: 'unordered-list',
action: SimpleMDE.toggleUnorderedList,
className: 'icon icon-unordered_list',
title: i18n('Unordered List'),
},
{
name: 'ordered-list',
action: SimpleMDE.toggleOrderedList,
className: 'icon icon-ordered_list',
title: i18n('Ordered List'),
},
'|',
{
name: 'code',
action: () => this.insertCodeBlock(),
className: 'icon icon-code',
title: i18n('Insert Code'),
default: true,
},
{
name: 'link',
action: SimpleMDE.drawLink,
className: 'icon icon-link',
title: i18n('Create Link'),
default: true,
},
{
name: 'image',
action: SimpleMDE.drawImage,
className: 'icon icon-insert--image',
title: i18n('Insert Image'),
default: true,
},
'|',
{
name: 'preview',
action: SimpleMDE.togglePreview,
preAction: SimpleMDE.preRenderPreview,
className: 'icon icon-preview no-disable',
title: i18n('Toggle Preview'),
default: true,
},
],
commonmark: {
safe: true,
},
...options,
});
}
async markdown(text) {
// FIXME preview wont update after first toggle
const data = await request.ajax({
url: '/markdown',
method: 'post',
data: $.param({ text }, true),
dataType: 'text',
});
_.defer(this.preparePreview.bind(this));
return data;
}
preparePreview() {
const $preview = $(this.wrapper).find('.simplemde-preview');
$preview.addClass('typo');
$preview.attr('data-emoji-enabled', 'true');
$preview.trigger('vjContentNew');
}
insertCodeBlock() {
const text = this.codemirror.getSelection();
const startPoint = this.codemirror.getCursor('start');
const endPoint = this.codemirror.getCursor('end');
const leadingLines = (startPoint.line === 0 && startPoint.ch === 0) ? 0 : 2;
// eslint-disable-next-line prefer-template
const startText = _.repeat('\n', leadingLines) + '```cpp\n';
const endText = '\n```\n';
this.codemirror.replaceSelection(`${startText}${text}${endText}`);
startPoint.line += leadingLines + 1;
startPoint.ch = 0;
if (startPoint !== endPoint) {
endPoint.line += leadingLines + 1;
}
this.codemirror.setSelection(startPoint, endPoint);
this.codemirror.focus();
}
}
@@ -0,0 +1,34 @@
.contest-type-tag
.contest-tag
font-size: rem($font-size-small)
display: inline-block
line-height: rem(22px)
padding: rem(0 10px)
vertical-align: middle
border-radius: rem(12px)
&:hover
text-decoration: none
color: #FFF !important
.contest-type-tag
&, &:visited
&, .supplementary &
color: #FFF
.icon
margin-right: rem(5px)
for key, value in $contest-color
.contest-type--{key} .contest-type-tag
background: value
&:hover
background: lighten(value, 20%)
.contest-tag
border: 1px solid $supplementary-border-color
&:hover
background: $primary-color
border-color: $primary-color
@@ -0,0 +1,21 @@
.contest-sidebar__bg
display: block
background: #67AABB url(problem-contest-bg.png) right top no-repeat
color: #FFF !important
font-size: rem($font-size)
padding: rem(20px 0)
+retina()
background-image: url(problem-contest-bg@2x.png)
background-size: 131px 150px
h1
font-size: rem($font-size-title)
color: #FFF
text-shadow: 0 1px 3px rgba(#000, 0.5)
&:hover
text-decoration: none
.contest-sidebar__status
margin-top: rem(20px)
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

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

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

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

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,68 @@
import _ from 'lodash';
import Slideout from 'slideout';
import { AutoloadPage } from 'vj/misc/Page';
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;
@@ -0,0 +1,232 @@
.nav--placeholder
height: $nav-item-height
.nav
position: relative
left: 0
top: 0
width: 100%
height: rem($nav-item-height)
font-size: rem($font-size-secondary)
transition: background .2s
transition-timing-function: ease-out-cubic
z-index: 500
.hasjs &
position: fixed
.nojs &
box-shadow: 0 0 10px rgba(#000, 0.4)
margin-bottom: rem(-($nav-item-height))
overflow: hidden
.nav > .row
transition: max-width .5s ease-out-cubic
.nav--shadow
position: fixed
left: 0
top: 0
width: 100%
height: rem($nav-item-height)
z-index: 300
transition: box-shadow .2s
transition-timing-function: ease-out-cubic
.nojs &
display: none
.nav__list, .nav__list-item
list-style: none
.nav__list--main
float: left
.nav__item
display: block
line-height: rem($nav-item-height - 6)
padding: rem(0 20px 3px 20px)
border-top: 3px solid transparent
color: #FFF
transition: border-color .2s, background .2s, color .2s
transition-timing-function: ease-out-cubic
position: relative
&:before
content: ''
position: absolute
left: 0
top: -1px
width: 100%
height: 1px
&:visited, &:active
color: #FFF
&.nav--active
border-color: $nav-item-active-immersive-color
&:before
box-shadow: 0 0 30px #FFF,
0 0 30px #FFF,
0 0 30px #FFF
&:hover
text-decoration: none
border-color: lighten($nav-item-hover-color, 60%)
background: rgba(#FFF, 0.3)
.nav__list--secondary
float: right
.nav__item--round
display: block
line-height: rem($nav-item-round-height - 2 * $nav-item-round-border)
margin: rem((($nav-item-height - $nav-item-round-height) / 2) 0)
padding: rem(0 25px)
border-radius: rem($nav-item-round-height / 2)
color: #FFF
border: $nav-item-round-border solid #FFF
transition: background .2s, color .2s, border-color .2s
transition-timing-function: ease-out-cubic
&:visited, &:active
color: #FFF
&:hover
text-decoration: none
background: #FFF
color: #666
.nav__list-item
float: left
.nav__list--main .nav__item--round
margin-right: rem(5px)
.nav__list--secondary .nav__item--round
margin-left: rem(5px)
.nav__logo__options
background-image: url(./nav-logo-small_light.png)
background-image: url(./nav-logo-small_dark.png)
background-image: url(./nav-logo-small@2x_light.png)
background-image: url(./nav-logo-small@2x_dark.png)
.nav__logo
display: block
width: 0
opacity: 0
height: rem($nav-logo-height)
margin: rem((($nav-item-height - $nav-logo-height) / 2) 0)
background: no-repeat
transition: width .3s, opacity .3s
transition-timing-function: ease-out-cubic
+retina()
background-size: $nav-logo-width $nav-logo-height
&:hover
text-decoration: none
+mobile()
display: none
.nav.showlogo, .layout--immersive
.nav__logo
width: rem($nav-logo-width + 10)
opacity: 1
.nav.floating, .nojs .nav
background: rgba(#FFF, 0.98)
.nav__item
color: $text-1-color
&.nav--active
border-color: $nav-item-active-color
&:before
box-shadow: 0 0 30px $nav-item-active-color,
0 0 30px $nav-item-active-color
&:hover
border-color: $nav-item-hover-color
background: #F8F8F8
&:before
box-shadow: none
.nav__item--round
border-color: $primary-color
color: $primary-color
&:hover
background-color: $primary-color
color: #FFF
.nav--shadow.floating
box-shadow: 0 0 6px rgba(#000, 0.3)
// For Mobile: show a slideout vertical arranged menu
+mobile()
.nav .columns
padding: 0
.nav--shadow
display: none
.nav.slideout-menu
background: #1D1F20
.nav__list
float: none
.nav__list-item
float: none
.nav__item,
.nav__item--round,
.nav__list--secondary .nav__item--round,
.nav .dropdown-target.menu .menu__link
line-height: rem($nav-item-height - 6)
padding: rem(0 20px 3px 20px)
border: 0
border-left: 3px solid transparent
margin: 0
border-radius: 0
background: none
color: #FFF
.icon
line-height: inherit
&.nav--active
border-color: $nav-item-active-color
&:visited, &:active
background: none
color: #FFF
&:hover
border-color: $nav-item-hover-color
background: rgba(#FFF, 0.10)
color: #FFF
.nav__item
&:before
left: -2px
top: 0
height: 100%
width: 1px
.nav .dropdown-target.menu
display: block
background: none
border: 0
box-shadow: none
.menu__link
padding-left: rem(35px)
.menu__seperator
display: none
.nav__list--secondary
border-top: 1px solid #666
@@ -0,0 +1,89 @@
import tpl from 'vj/utils/tpl';
import zIndexManager from 'vj/utils/zIndexManager';
let lastNotification = null;
let autoHideTimer = null;
export default class Notification {
constructor({
avatar, title, message, type = '', duration = 3000, action,
}) {
this.type = type;
if (avatar) this.type += ' avatar';
if (title) this.type += ' title';
this.action = (() => { }) || action;
this.$dom = $(tpl`<div class="notification ${type} hide"></div>`);
if (avatar) $(tpl`<img width="64" height="64" class="avatar" src="${avatar}"></img>`).appendTo(this.$dom);
if (title) $(tpl`<h2>${title}</h2>`).appendTo(this.$dom);
$(tpl`<p>${message}</p>`).appendTo(this.$dom);
this.$dom.on('clink', this.handleClick.bind(this));
this.$n = this.$dom
.css('z-index', zIndexManager.getNext())
.appendTo('body');
this.$n.width(); // force reflow
this.$n.removeClass('hide');
if (duration) this.autoHideTimer = setTimeout(this.hide.bind(this), duration);
}
handleClick() {
this.action();
}
hide() {
this.$n.addClass('hide');
setTimeout(() => this.$n.remove(), 200);
}
static success(message, duration) {
return Notification.show({ type: 'success', message, duration });
}
static info(message, duration) {
return Notification.show({ type: 'info', message, duration });
}
static warn(message, duration) {
return Notification.show({ type: 'warn', message, duration });
}
static error(message, duration) {
return Notification.show({ type: 'error', message, duration });
}
static show({
avatar, title, message, type = '', duration = 3000, action,
}) {
if (lastNotification) Notification.hide();
if (avatar) type += ' avatar';
if (title) type += ' title';
const $dom = $(tpl`<div class="notification ${type} hide"></div>`);
if (avatar) $(tpl`<img width="64" height="64" class="avatar" src="${avatar}"></img>`).appendTo($dom);
if (title) $(tpl`<h2>${title}</h2>`).appendTo($dom);
$(tpl`<p>${message}</p>`).appendTo($dom);
$dom.on('clink', action);
const $n = $dom
.css('z-index', zIndexManager.getNext())
.appendTo('body');
$n.width(); // force reflow
$n.removeClass('hide');
lastNotification = $n;
autoHideTimer = setTimeout(Notification.hide, duration);
return $n;
}
static hide() {
if (!lastNotification) {
return;
}
if (autoHideTimer) {
clearTimeout(autoHideTimer);
autoHideTimer = null;
}
const $n = lastNotification;
$n.addClass('hide');
setTimeout(() => $n.remove(), 200);
lastNotification = null;
}
}
window.Hydro.components.Notification = Notification;
@@ -0,0 +1,49 @@
.notification
position: fixed
left: rem(20px)
bottom: rem(20px)
padding: rem(10px)
border-radius: rem(2px)
background: rgba(#222, 0.9)
color: #FFF
font-size: rem($font-size-secondary)
transition: transform .2s, opacity .2s
transition-timing-function: ease-out-cubic
z-index: 1000 // managed
line-height: rem(20px)
box-shadow: 0 4px 10px rgba(#000, 0.4)
min-width: rem(150px)
img
border-radius: 50%
&.hide
opacity: 0
transform: translateY(20px) scale(0.9)
&:before
display: inline-block
font-family: $icon-font-name !important
margin-right: rem(10px)
font-size: rem($font-size-icon)
line-height: 1
vertical-align: top
margin-top: rem((20px - $font-size-icon) / 2)
for key, value in $notification-color
&.{key}
border-left: 5px solid value
&:before
color: value
for key, value in $notification-icon
&.{key}:before
content: value
+mobile()
.notification
padding: rem(20px 15px)
left: 0
width: 100%
min-width: auto
bottom: 0
@@ -0,0 +1,3 @@
import NProgress from 'nprogress';
export default NProgress;
@@ -0,0 +1,69 @@
$spinner-size = 18px
$spinner-margin = ($nav-item-height - $spinner-size) / 2
#nprogress
pointer-events: none
#nprogress .bar
background: $nprogress-color
position: fixed
z-index: 1031
top: 0
left: 0
width: 100%
height: 2px
.nav--floating &
+above(rupture.mobile-cutoff)
background: $nprogress-color-floating
#nprogress .peg
display: block
position: absolute
right: 0px
width: 100px
height: 100%
box-shadow: 0 0 10px $nprogress-color, 0 0 5px $nprogress-color
opacity: 1.0
transform: rotate(3deg) translate(0px, -4px)
.nav--floating &
+above(rupture.mobile-cutoff)
box-shadow: 0 0 10px $nprogress-color-floating, 0 0 5px $nprogress-color-floating
#nprogress .spinner
display: block
position: fixed
z-index: 1031
top: rem($spinner-margin)
right: rem($spinner-margin)
#nprogress .spinner-icon
width: rem($spinner-size)
height: rem($spinner-size)
box-sizing: border-box
border: solid 2px transparent
border-top-color: $nprogress-color
border-left-color: $nprogress-color
border-radius: 50%
animation: nprogress-spinner 400ms linear infinite
.nav--floating &
+above(rupture.mobile-cutoff)
border-top-color: $nprogress-color-floating
border-left-color: $nprogress-color-floating
.nprogress-custom-parent
overflow: hidden
position: relative
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar
position: absolute
@keyframes nprogress-spinner
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
@@ -0,0 +1,46 @@
.pager
margin-bottom: rem(10px)
border-top: 2px solid $pager-border-color
text-align: center
font-size: rem($font-size-small)
&:empty
display: none
li
margin: 0
display: inline-block
margin-top: -2px
.pager__item
display: inline-block
padding: rem(10px)
text-decoration: none
border: 0
border-top: 2px solid $pager-border-color
transition: border-top .2s linear, color .2s linear
color: #a7a7a7
&.link
&, .typo &
color: #a7a7a7
&:hover
border-color: $primary-color
text-decoration: none
&, .typo &
color: #666
&.current
color: $secondary-color
border-color: $secondary-color
+mobile()
.pager__item
&.ellipsis,
&.first,
&.previous,
&.next,
&.last
display: none
@@ -0,0 +1,4 @@
.problem__rp-tag
font-size: 0.8em
padding: rem(0 5px)
color: #AAA
@@ -0,0 +1,37 @@
.problem__tags
margin: 0 !important
padding: 0 !important
list-style: none !important
font-size: 0
.problem__tag
display: inline-block
vertical-align: top
height: rem($table-lh)
margin: rem(0 2px 2px 0) !important
padding: 0 !important
.problem__tag-link
display: inline-block
padding: rem(0 7px)
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
max-width: rem(130px)
line-height: rem($table-lh)
height: rem($table-lh)
background: #F0F0F0
color: $text-1-color !important
font-size: rem($font-size-small)
&:hover
background: $primary-color
color: #FFF !important
text-decoration: none
.col--problem-name
.problem__tags
float: right
.problem__tag
margin: rem(0 2px 0 0) !important
Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Loaded 100 of 564 files, more files were not shown because too many files have changed in this diff. Show more