ui-default: init
将ui-default合并至主存储库便于维护
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)
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,21 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const datepickerPage = new AutoloadPage('datepickerPage', async () => {
|
||||
if ($('[data-pick-date]').length > 0) {
|
||||
await import('pickadate/lib/picker.date');
|
||||
$('[data-pick-date]').pickadate({
|
||||
format: 'yyyy-m-d',
|
||||
clear: false,
|
||||
});
|
||||
}
|
||||
if ($('[data-pick-time]').length > 0) {
|
||||
await import('pickadate/lib/picker.time');
|
||||
$('[data-pick-time]').pickatime({
|
||||
format: 'H:i',
|
||||
interval: 15,
|
||||
clear: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default datepickerPage;
|
||||
@@ -0,0 +1,54 @@
|
||||
.picker__holder
|
||||
margin-top: -1rem
|
||||
border-radius: 0
|
||||
transform: scale(0.9) translateZ(0)
|
||||
transform-origin: 0 0
|
||||
outline: 0
|
||||
font-size: rem($font-size-secondary)
|
||||
transition: transform .1s ease-in-out,
|
||||
opacity .1s ease-in-out,
|
||||
max-height 0s .1s,
|
||||
border-width 0s .1s
|
||||
|
||||
.picker--opened .picker__holder
|
||||
transform: scale(1) translateZ(0)
|
||||
transition: transform .1s ease-in-out,
|
||||
opacity .1s ease-in-out,
|
||||
max-height 0s,
|
||||
border-width 0s
|
||||
box-shadow: $data-picker-shadow
|
||||
|
||||
.picker__list
|
||||
padding: 0
|
||||
|
||||
.picker__list-item
|
||||
border: 0
|
||||
|
||||
.picker__list-item:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__list-item--selected, .picker__list-item--selected:hover, .picker--focused .picker__list-item--selected
|
||||
background: $primary-color
|
||||
|
||||
.picker__day--infocus:hover, .picker__day--outfocus:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__day--highlighted
|
||||
border-color: transparent
|
||||
|
||||
.picker__day--highlighted:hover, .picker--focused .picker__day--highlighted
|
||||
background: transparent
|
||||
|
||||
.picker__day--selected, .picker__day--selected:hover, .picker--focused .picker__day--selected
|
||||
background: $primary-color
|
||||
|
||||
.picker__button--today:hover, .picker__button--clear:hover, .picker__button--close:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
border-color: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__button--today:focus, .picker__button--clear:focus, .picker__button--close:focus
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
border-color: $datapicker-list-item-bg-color-hover
|
||||
|
||||
.picker__nav--prev:hover, .picker__nav--next:hover
|
||||
background: $datapicker-list-item-bg-color-hover
|
||||
@@ -0,0 +1,145 @@
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
import zIndexManager from 'vj/utils/zIndexManager';
|
||||
|
||||
export default class DomDialog extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjDomDialogInstance';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
super($dom);
|
||||
this.isShown = false;
|
||||
this.isAnimating = false;
|
||||
this.options = {
|
||||
cancelByClickingBack: false,
|
||||
cancelByEsc: false,
|
||||
onDispatch: () => {},
|
||||
...options,
|
||||
};
|
||||
this._defer = null;
|
||||
}
|
||||
|
||||
async _show() {
|
||||
this.$dom.css('z-index', zIndexManager.getNext());
|
||||
this.$dom.trigger('vjDomDialogShow');
|
||||
this.isAnimating = true;
|
||||
|
||||
if (this.options.cancelByClickingBack) {
|
||||
this.$dom.on(`click.${this.eventNS}`, this.handleClick.bind(this));
|
||||
}
|
||||
if (this.options.cancelByEsc) {
|
||||
$(document).on(`keyup.${this.eventNS}`, this.handleKeyUp.bind(this));
|
||||
}
|
||||
|
||||
const $wrap = this.$dom;
|
||||
$wrap.css({
|
||||
display: 'flex',
|
||||
opacity: 0,
|
||||
});
|
||||
$wrap.width();
|
||||
$wrap.transition({
|
||||
opacity: 1,
|
||||
}, {
|
||||
duration: 100,
|
||||
easing: 'easeOutCubic',
|
||||
});
|
||||
|
||||
const $dgContent = this.$dom.find('.dialog__content');
|
||||
$dgContent.css({
|
||||
scale: 0.8,
|
||||
});
|
||||
await $dgContent
|
||||
.transition({
|
||||
scale: 1,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
this.$dom.find('[data-autofocus]').focus();
|
||||
|
||||
this.isShown = true;
|
||||
this.isAnimating = false;
|
||||
this.$dom.trigger('vjDomDialogShown');
|
||||
}
|
||||
|
||||
async _hide() {
|
||||
this.$dom.trigger('vjDomDialogHide');
|
||||
this.isAnimating = true;
|
||||
|
||||
$(document).off(`keyup.${this.eventNS}`);
|
||||
this.$dom.off(`click.${this.eventNS}`);
|
||||
|
||||
this.$dom.css({
|
||||
opacity: 1,
|
||||
});
|
||||
this.$dom.transition({
|
||||
opacity: 0,
|
||||
}, {
|
||||
duration: 200,
|
||||
});
|
||||
|
||||
const $dgContent = this.$dom.find('.dialog__content');
|
||||
$dgContent.css({
|
||||
scale: 1,
|
||||
});
|
||||
await $dgContent
|
||||
.transition({
|
||||
scale: 0.8,
|
||||
}, {
|
||||
duration: 200,
|
||||
easing: 'easeOutCubic',
|
||||
})
|
||||
.promise();
|
||||
|
||||
this.$dom.css('display', 'none');
|
||||
|
||||
this.isShown = false;
|
||||
this.isAnimating = false;
|
||||
this.$dom.trigger('vjDomDialogHidden');
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.isShown || this.isAnimating) {
|
||||
return Promise.reject();
|
||||
}
|
||||
this._defer = new $.Deferred();
|
||||
this._show();
|
||||
return this._defer.promise();
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.isShown || this.isAnimating) {
|
||||
return false;
|
||||
}
|
||||
if (this._defer.state() === 'pending') {
|
||||
this._defer.resolve('cancel');
|
||||
}
|
||||
this._defer = null;
|
||||
this._hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatchAction(data) {
|
||||
if (this.options.onDispatch(data) === false) {
|
||||
return;
|
||||
}
|
||||
this._defer.resolve(data);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
if (e.target === this.$dom.get(0)) {
|
||||
this.dispatchAction('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.dispatchAction('cancel');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(DomDialog, DOMAttachedObject);
|
||||
@@ -0,0 +1,38 @@
|
||||
.dialog
|
||||
position: fixed
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
opacity: 0
|
||||
z-index: 1000 // managed
|
||||
// display flex will be added after animation completes
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
display: none
|
||||
background: $dialog-layer-bg-color
|
||||
|
||||
.dialog__content
|
||||
position: relative
|
||||
|
||||
.dialog.withBg .dialog__content
|
||||
background: $dialog-bg-color
|
||||
border: 1px solid $dialog-border-color
|
||||
box-shadow: $dialog-shadow
|
||||
min-width: rem(500px)
|
||||
max-width: rem(900px)
|
||||
padding: rem(40px) rem(30px)
|
||||
|
||||
.dialog__body
|
||||
margin-bottom: rem(20px)
|
||||
min-height: rem(50px)
|
||||
|
||||
h1
|
||||
font-size: 1.6rem
|
||||
color: $dialog-title-color
|
||||
margin-bottom: 1rem
|
||||
|
||||
.dialog__action button
|
||||
margin-left: rem(10px)
|
||||
margin-bottom: 0
|
||||
@@ -0,0 +1,99 @@
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import DomDialog from './DomDialog';
|
||||
|
||||
export default class Dialog {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
classes: '',
|
||||
$body: null,
|
||||
$action: null,
|
||||
...options,
|
||||
};
|
||||
this.$dom = $(tpl`
|
||||
<div class="dialog withBg ${this.options.classes}" style="display:none">
|
||||
<div class="dialog__content">
|
||||
<div class="dialog__body"></div>
|
||||
<div class="row"><div class="columns clearfix">
|
||||
<div class="float-right dialog__action"></div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
this.$dom.on('click', '[data-action]', this.handleActionButton.bind(this));
|
||||
this.$dom.on('vjDomDialogShow', this.beforeShow.bind(this));
|
||||
this.$dom.on('vjDomDialogHidden', this.afterHide.bind(this));
|
||||
this.$dom.find('.dialog__body').append(this.options.$body);
|
||||
this.$dom.find('.dialog__action').append(this.options.$action);
|
||||
this.domDialogInstance = new DomDialog(this.$dom, this.options);
|
||||
}
|
||||
|
||||
beforeShow() {
|
||||
this.$dom.appendTo('body');
|
||||
}
|
||||
|
||||
afterHide() {
|
||||
this.$dom.detach();
|
||||
}
|
||||
|
||||
open() {
|
||||
return this.domDialogInstance.show();
|
||||
}
|
||||
|
||||
close() {
|
||||
return this.domDialogInstance.hide();
|
||||
}
|
||||
|
||||
handleActionButton(ev) {
|
||||
this.domDialogInstance.dispatchAction($(ev.currentTarget).attr('data-action'));
|
||||
}
|
||||
}
|
||||
|
||||
const buttonOk = tpl`<button class="primary rounded button" data-action="ok">${i18n('Ok')}</button>`;
|
||||
const buttonCancel = tpl`<button class="rounded button" data-action="cancel">${i18n('Cancel')}</button>`;
|
||||
const buttonYes = tpl`<button class="primary rounded button" data-action="yes">${i18n('Yes')}</button>`;
|
||||
const buttonNo = tpl`<button class="rounded button" data-action="no">${i18n('No')}</button>`;
|
||||
|
||||
export class InfoDialog extends Dialog {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
$action: buttonOk,
|
||||
cancelByClickingBack: true,
|
||||
cancelByEsc: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionDialog extends Dialog {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
$action: [buttonCancel, buttonOk].join('\n'),
|
||||
cancelByClickingBack: true,
|
||||
cancelByEsc: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmDialog extends Dialog {
|
||||
constructor(options = {}) {
|
||||
let buttons = [];
|
||||
if (options.canCancel) {
|
||||
buttons = [buttonCancel, buttonNo, buttonYes];
|
||||
} else {
|
||||
buttons = [buttonNo, buttonYes];
|
||||
}
|
||||
super({
|
||||
$action: buttons.join('\n'),
|
||||
cancelByClickingBack: options.canCancel,
|
||||
cancelByEsc: options.canCancel,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.Hydro.components.Dialog = Dialog;
|
||||
window.Hydro.components.InfoDialog = InfoDialog;
|
||||
window.Hydro.components.ActionDialog = ActionDialog;
|
||||
window.Hydro.components.ConfirmDialog = ConfirmDialog;
|
||||
@@ -0,0 +1,105 @@
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import request from 'vj/utils/request';
|
||||
import TextareaHandler from 'vj/components/cmeditor/textareaHandler';
|
||||
import Notification from 'vj/components/notification';
|
||||
|
||||
let initialized = false;
|
||||
const $template = $('.dczcomments__box').eq(0).clone();
|
||||
|
||||
function getClosestInstance($dom) {
|
||||
return $dom.closest('.dczcomments__box').data('instance');
|
||||
}
|
||||
|
||||
function onBoxCancel(ev) {
|
||||
const commentBox = getClosestInstance($(ev.currentTarget));
|
||||
if (commentBox) commentBox.onCancel(ev);
|
||||
}
|
||||
|
||||
function onBoxSubmit(ev) {
|
||||
const commentBox = getClosestInstance($(ev.currentTarget));
|
||||
if (commentBox) commentBox.onSubmit(ev);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (initialized) return;
|
||||
$(document).on('submit', '[name="dczcomments__box__form"]', onBoxSubmit);
|
||||
$(document).on('vjCommentBoxCancel', '[name="dczcomments__box__form"]', onBoxCancel);
|
||||
$(document).on('click', '[name="dczcomments__box__cancel"]', onBoxCancel);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
export default class CommentBox extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjCommentBoxInstance';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
super($dom);
|
||||
init();
|
||||
this.$box = $template.clone();
|
||||
this.$box.data('instance', this);
|
||||
this.options = {
|
||||
initialText: null,
|
||||
mode: null,
|
||||
form: null,
|
||||
onCancel: () => null,
|
||||
...options,
|
||||
};
|
||||
if (this.options.initialText) this.setText(this.options.initialText);
|
||||
if (this.options.mode) {
|
||||
const submitButton = this.$box.find('.dczcomments__box__submit');
|
||||
submitButton.val(submitButton.attr(`data-value-${this.options.mode}`));
|
||||
}
|
||||
}
|
||||
|
||||
getTextareaHandler() {
|
||||
const $textarea = this.$box.find('textarea');
|
||||
return TextareaHandler.getOrConstruct($textarea);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.getTextareaHandler().focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
setText(text) {
|
||||
this.getTextareaHandler().val(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
getText() {
|
||||
return this.getTextareaHandler().val();
|
||||
}
|
||||
|
||||
insertText(text) {
|
||||
const handler = this.getTextareaHandler();
|
||||
handler.val(handler.val() + text);
|
||||
return this;
|
||||
}
|
||||
|
||||
appendTo($dom) {
|
||||
this.$box.appendTo($dom);
|
||||
this.$box.trigger('vjContentNew');
|
||||
return this;
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
try {
|
||||
await request.post('', {
|
||||
...this.options.form,
|
||||
content: this.getText(),
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
Notification.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async onCancel(ev) {
|
||||
await this.options.onCancel(ev);
|
||||
this.$box.remove();
|
||||
this.detach();
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(CommentBox, DOMAttachedObject);
|
||||
@@ -0,0 +1,206 @@
|
||||
import 'jquery.easing';
|
||||
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import CommentBox from 'vj/components/discussion/CommentBox';
|
||||
import { ConfirmDialog } from 'vj/components/dialog';
|
||||
|
||||
import delay from 'vj/utils/delay';
|
||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||
import request from 'vj/utils/request';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
|
||||
const $replyTemplate = $('.commentbox-container').eq(0).clone();
|
||||
|
||||
function createReplyContainer($parent) {
|
||||
const $container = $replyTemplate
|
||||
.clone()
|
||||
.hide()
|
||||
.prependTo($parent.find('.commentbox-reply-target').eq(0))
|
||||
.trigger('vjContentNew');
|
||||
return $container.find('.commentbox-placeholder');
|
||||
}
|
||||
|
||||
async function showReplyContainer($parent) {
|
||||
const $container = $parent.find('.commentbox-container');
|
||||
// TODO: fix ugly hack. cannot get $container rect because it is invisible
|
||||
const rect = $container.parent()[0].getBoundingClientRect();
|
||||
const rectBody = document.body.getBoundingClientRect();
|
||||
if (rect.top < 100 || rect.top + 100 > window.innerHeight) {
|
||||
const targetScrollTop = rect.top - rectBody.top - window.innerHeight * 0.382;
|
||||
$('html, body').stop().animate({ scrollTop: targetScrollTop }, 400, 'easeOutCubic');
|
||||
await delay(300);
|
||||
// delay duration is set smaller than animation duration intentionally
|
||||
}
|
||||
$container.css('opacity', 0);
|
||||
await slideDown($container, 300);
|
||||
await $container.transition({ opacity: 1 }, { duration: 200 }).promise();
|
||||
$container.removeAttr('style');
|
||||
}
|
||||
|
||||
async function destroyReplyContainer($parent) {
|
||||
const $container = $parent.find('.commentbox-container');
|
||||
$container.css('opacity', 1);
|
||||
await $container.transition({ opacity: 0 }, { duration: 200 }).promise();
|
||||
await slideUp($container, 300);
|
||||
$container.remove();
|
||||
}
|
||||
|
||||
function onClickDummyBox(ev) {
|
||||
const $evTarget = $(ev.currentTarget);
|
||||
|
||||
if (CommentBox.get($evTarget)) {
|
||||
CommentBox
|
||||
.get($evTarget)
|
||||
.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const $mediaBody = $evTarget.closest('.media__body');
|
||||
|
||||
const opt = {
|
||||
form: JSON.parse($evTarget.attr('data-form')),
|
||||
mode: 'comment',
|
||||
onCancel: () => {
|
||||
$mediaBody.removeClass('is-editing');
|
||||
},
|
||||
};
|
||||
|
||||
$mediaBody.addClass('is-editing');
|
||||
|
||||
CommentBox
|
||||
.getOrConstruct($evTarget, opt)
|
||||
.appendTo($mediaBody.find('.commentbox-placeholder').eq(0))
|
||||
.focus();
|
||||
}
|
||||
|
||||
async function onCommentClickReplyComment(ev, options = {}) {
|
||||
const $evTarget = $(ev.currentTarget);
|
||||
|
||||
if (CommentBox.get($evTarget)) {
|
||||
// If comment box is already expanded,
|
||||
// we should insert "initialText"
|
||||
CommentBox
|
||||
.get($evTarget)
|
||||
.insertText(options.initialText || '')
|
||||
.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const $mediaBody = $evTarget.closest('.media__body');
|
||||
|
||||
const opt = {
|
||||
initialText: '',
|
||||
mode: 'reply',
|
||||
...options,
|
||||
onCancel: async () => {
|
||||
await destroyReplyContainer($mediaBody);
|
||||
},
|
||||
};
|
||||
|
||||
const cbox = CommentBox
|
||||
.getOrConstruct($evTarget, {
|
||||
form: JSON.parse($evTarget.attr('data-form')),
|
||||
...opt,
|
||||
})
|
||||
.appendTo(createReplyContainer($mediaBody));
|
||||
await showReplyContainer($mediaBody);
|
||||
cbox.focus();
|
||||
}
|
||||
|
||||
async function onCommentClickReplyReply(ev) {
|
||||
const $evTarget = $(ev.currentTarget);
|
||||
const $mediaBody = $evTarget.closest('.media__body');
|
||||
const username = $mediaBody
|
||||
.find('.user-profile-name').eq(0)
|
||||
.text();
|
||||
|
||||
$evTarget
|
||||
.closest('.dczcomments__item')
|
||||
.find('[name="dczcomments__op-reply-comment"]').eq(0)
|
||||
.trigger('click', { initialText: `@${username}: ` });
|
||||
}
|
||||
|
||||
async function onCommentClickEdit(mode, ev) {
|
||||
const $evTarget = $(ev.currentTarget);
|
||||
|
||||
if (CommentBox.get($evTarget)) {
|
||||
CommentBox
|
||||
.get($evTarget)
|
||||
.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const $mediaBody = $evTarget.closest('.media__body');
|
||||
|
||||
const raw = await request.get(
|
||||
$mediaBody.find('.typo').eq(0).attr('data-raw-url'),
|
||||
{},
|
||||
{ dataType: 'text' },
|
||||
);
|
||||
|
||||
const opt = {
|
||||
initialText: raw,
|
||||
form: JSON.parse($evTarget.attr('data-form')),
|
||||
mode,
|
||||
onCancel: () => {
|
||||
$mediaBody.removeClass('is-editing');
|
||||
},
|
||||
};
|
||||
|
||||
$mediaBody.addClass('is-editing');
|
||||
|
||||
CommentBox
|
||||
.getOrConstruct($evTarget, opt)
|
||||
.appendTo($mediaBody.find('.commentbox-edit-target').eq(0))
|
||||
.focus();
|
||||
}
|
||||
|
||||
function onCommentClickEditComment(ev) {
|
||||
return onCommentClickEdit('comment-update', ev);
|
||||
}
|
||||
|
||||
function onCommentClickEditReply(ev) {
|
||||
return onCommentClickEdit('reply-update', ev);
|
||||
}
|
||||
|
||||
async function onCommentClickDelete(type, ev) {
|
||||
const message = (type === 'comment')
|
||||
? 'Confirm deleting this comment? Its replies will be deleted as well.'
|
||||
: 'Confirm deleting this reply?';
|
||||
const action = await new ConfirmDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n(message)}</p>
|
||||
</div>`,
|
||||
}).open();
|
||||
if (action !== 'yes') {
|
||||
return;
|
||||
}
|
||||
|
||||
const $evTarget = $(ev.currentTarget);
|
||||
const form = JSON.parse($evTarget.attr('data-form'));
|
||||
|
||||
await request.post('', form);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function onCommentClickDeleteComment(ev) {
|
||||
onCommentClickDelete('comment', ev);
|
||||
}
|
||||
|
||||
function onCommentClickDeleteReply(ev) {
|
||||
onCommentClickDelete('reply', ev);
|
||||
}
|
||||
|
||||
const commentsPage = new AutoloadPage('commentsPage', () => {
|
||||
$(document).on('click', '[name="dczcomments__dummy-box"]', onClickDummyBox);
|
||||
$(document).on('click', '[name="dczcomments__op-reply-comment"]', onCommentClickReplyComment);
|
||||
$(document).on('click', '[name="dczcomments__op-reply-reply"]', onCommentClickReplyReply);
|
||||
$(document).on('click', '[name="dczcomments__op-edit-comment"]', onCommentClickEditComment);
|
||||
$(document).on('click', '[name="dczcomments__op-edit-reply"]', onCommentClickEditReply);
|
||||
$(document).on('click', '[name="dczcomments__op-delete-comment"]', onCommentClickDeleteComment);
|
||||
$(document).on('click', '[name="dczcomments__op-delete-reply"]', onCommentClickDeleteReply);
|
||||
});
|
||||
|
||||
export default commentsPage;
|
||||
@@ -0,0 +1,125 @@
|
||||
$upvote-color = green
|
||||
$downvote-color = red
|
||||
|
||||
.dczcomments__item .is-editing
|
||||
[name="dczcomments__dummy-box"]
|
||||
display: none
|
||||
> .typo
|
||||
display: none
|
||||
|
||||
.dczcomments__box .textbox
|
||||
height: rem(180px)
|
||||
resize: vertical
|
||||
|
||||
.textbox[name="dczcomments__dummy-box"]
|
||||
height: rem(60px)
|
||||
resize: none
|
||||
font-size: rem($font-size-title)
|
||||
color: $comment-placeholder-color
|
||||
padding: rem(10px)
|
||||
|
||||
.dczcomments__item,
|
||||
.dczcomments__reply
|
||||
border-top: 1px solid $table-border-color
|
||||
|
||||
>.media
|
||||
padding: rem(20px 0)
|
||||
|
||||
&:first-child
|
||||
border-top: 0
|
||||
|
||||
.dczcomments__reply
|
||||
&:last-child
|
||||
>.media
|
||||
padding-bottom: 0
|
||||
|
||||
.dczcomments__supplementary
|
||||
float: left
|
||||
margin-bottom: rem(5px)
|
||||
|
||||
.dczcomments__operations
|
||||
font-size: rem($font-size)
|
||||
float: right
|
||||
vertical-align: top
|
||||
color: $comment-op-color
|
||||
|
||||
a, a:visited
|
||||
color: $comment-op-link-color
|
||||
margin-left: rem(3px)
|
||||
a:hover
|
||||
color: $primary-color
|
||||
text-decoration: none
|
||||
|
||||
.view--solution .dczcomments__replies
|
||||
margin-left: rem(50px)
|
||||
|
||||
.dczcomments__item
|
||||
.vote-number
|
||||
width: rem(82px)
|
||||
text-align: center
|
||||
font-size: rem(25px)
|
||||
font-weight: 300
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
.vote-op
|
||||
position: relative
|
||||
border: 1px solid transparent
|
||||
border-radius: rem(14px)
|
||||
transition: border-color .2s linear, opacity .2s linear
|
||||
opacity: 0.5
|
||||
|
||||
&>form
|
||||
width: rem(40px)
|
||||
float: left
|
||||
position: relative
|
||||
|
||||
&:hover .vote-op
|
||||
opacity: 1
|
||||
border-color: $supplementary-border-color
|
||||
|
||||
.vote-button
|
||||
appearance: none
|
||||
border: 0
|
||||
background: none
|
||||
width: 100%
|
||||
outline: 0
|
||||
font-size: rem($font-size-icon)
|
||||
line-height: rem(26px)
|
||||
color: #AAA
|
||||
transition: color .2s linear, background-color .2s linear, box-shadow .2s linear
|
||||
|
||||
&.upvote
|
||||
border-radius: rem(13px 0 0 13px)
|
||||
padding-left: rem(7px)
|
||||
|
||||
&:hover
|
||||
background: $upvote-color
|
||||
color: #FFF
|
||||
box-shadow: 0 2px 10px rgba($upvote-color, 0.4)
|
||||
|
||||
&.active
|
||||
color: $upvote-color
|
||||
|
||||
|
||||
&.downvote
|
||||
border-radius: rem(0 13px 13px 0)
|
||||
padding-right: rem(7px)
|
||||
|
||||
&:hover
|
||||
background: $downvote-color
|
||||
color: #FFF
|
||||
box-shadow: 0 2px 10px rgba($downvote-color, 0.4)
|
||||
|
||||
&.active
|
||||
color: $downvote-color
|
||||
|
||||
&.active
|
||||
background: transparent !important
|
||||
box-shadow: none !important
|
||||
cursor: default
|
||||
|
||||
|
||||
+mobile()
|
||||
.dczcomments__item
|
||||
.media__left
|
||||
display: none
|
||||
@@ -0,0 +1,17 @@
|
||||
.discussion-node-tag
|
||||
font-size: rem($font-size-small)
|
||||
display: inline-block
|
||||
line-height: rem(22px)
|
||||
padding: rem(0 10px)
|
||||
vertical-align: middle
|
||||
border-radius: rem(12px)
|
||||
border: 1px solid $supplementary-border-color
|
||||
|
||||
&:hover
|
||||
background: $primary-color
|
||||
color: #FFF !important
|
||||
text-decoration: none
|
||||
border-color: $primary-color
|
||||
|
||||
.icon
|
||||
margin-right: rem(5px)
|
||||
@@ -0,0 +1,40 @@
|
||||
.drop-element,
|
||||
.drop-element:after,
|
||||
.drop-element:before,
|
||||
.drop-element *,
|
||||
.drop-element *:after,
|
||||
.drop-element *:before
|
||||
box-sizing: border-box
|
||||
|
||||
.drop-element
|
||||
position: absolute
|
||||
display: none
|
||||
opacity: 0
|
||||
transition: opacity .1s ease-in-out
|
||||
z-index: 1000 // managed
|
||||
|
||||
&.drop-open
|
||||
display: block
|
||||
|
||||
&.drop-open-transitionend
|
||||
display: block
|
||||
|
||||
&.drop-after-open
|
||||
opacity: 1
|
||||
|
||||
.drop-target-attached-top.drop-target-attached-left .drop-content
|
||||
transform-origin: -10% 110%
|
||||
.drop-target-attached-top.drop-target-attached-center .drop-content
|
||||
transform-origin: 50% 110%
|
||||
.drop-target-attached-top.drop-target-attached-right .drop-content
|
||||
transform-origin: 110% 110%
|
||||
.drop-target-attached-bottom.drop-target-attached-left .drop-content
|
||||
transform-origin: -10% -10%
|
||||
.drop-target-attached-bottom.drop-target-attached-center .drop-content
|
||||
transform-origin: 50% -10%
|
||||
.drop-target-attached-bottom.drop-target-attached-right .drop-content
|
||||
transform-origin: 110% -10%
|
||||
.drop-target-attached-center.drop-target-attached-right .drop-content
|
||||
transform-origin: -10% 50%
|
||||
.drop-target-attached-center.drop-target-attached-left .drop-content
|
||||
transform-origin: 110% 50%
|
||||
@@ -0,0 +1,55 @@
|
||||
import Drop from 'tether-drop';
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import responsiveCutoff from 'vj/breakpoints.json';
|
||||
|
||||
import zIndexManager from 'vj/utils/zIndexManager';
|
||||
import { isBelow } from 'vj/utils/mediaQuery';
|
||||
|
||||
export default class Dropdown extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjDropdownInstance';
|
||||
|
||||
static DOMAttachSelector = '[data-dropdown-target]';
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
if ($dom.attr('data-dropdown-trigger-desktop-only') !== undefined) {
|
||||
if (isBelow(responsiveCutoff.mobile)) {
|
||||
super(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
super($dom);
|
||||
this.options = {
|
||||
target: null,
|
||||
position: $dom.attr('data-dropdown-pos') || 'bottom left',
|
||||
...options,
|
||||
};
|
||||
this.dropInstance = new Drop({
|
||||
target: $dom[0],
|
||||
classes: 'dropdown',
|
||||
content: this.options.target || $.find($dom.attr('data-dropdown-target'))[0],
|
||||
position: this.options.position,
|
||||
openOn: 'hover',
|
||||
constrainToWindow: true,
|
||||
constrainToScrollParent: false,
|
||||
});
|
||||
this.dropInstance.on('open', this.onDropOpen.bind(this));
|
||||
this.dropInstance.on('close', this.onDropClose.bind(this));
|
||||
}
|
||||
|
||||
detach() {
|
||||
super.detach();
|
||||
this.dropInstance.destroy();
|
||||
}
|
||||
|
||||
onDropOpen() {
|
||||
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
|
||||
this.$dom.trigger('vjDropdownShow');
|
||||
}
|
||||
|
||||
onDropClose() {
|
||||
this.$dom.trigger('vjDropdownHide');
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(Dropdown, DOMAttachedObject);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import Dropdown from './Dropdown';
|
||||
|
||||
const dropdownPage = new AutoloadPage('dropdownPage', () => {
|
||||
Dropdown.registerLifeCycleHooks();
|
||||
});
|
||||
|
||||
export default dropdownPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
.dropdown-target
|
||||
display: none
|
||||
|
||||
.drop .dropdown-target
|
||||
display: block
|
||||
|
||||
.dropdown
|
||||
.menu
|
||||
min-width: rem(150px)
|
||||
box-shadow: $menu-drop-shadow
|
||||
background: $menu-drop-bg-color
|
||||
font-size: rem($font-size-small)
|
||||
|
||||
.menu__link
|
||||
padding: rem(8px 10px)
|
||||
|
||||
.dropdown
|
||||
.drop-content
|
||||
transition: transform .1s ease-in-out
|
||||
transform: scale(0.9) translateZ(0)
|
||||
|
||||
&.drop-after-open
|
||||
.drop-content
|
||||
transform: scale(1) translateZ(0)
|
||||
@@ -0,0 +1,6 @@
|
||||
.emoji
|
||||
width: 1em
|
||||
height: 1em
|
||||
display: inline-block
|
||||
background-size: contain
|
||||
vertical-align: baseline
|
||||
@@ -0,0 +1,21 @@
|
||||
import emojify from 'emojify.js';
|
||||
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
function runEmojify($container) {
|
||||
if ($container.is('[data-emoji-enabled]')) {
|
||||
emojify.run($container[0]);
|
||||
return;
|
||||
}
|
||||
$container.find('[data-emoji-enabled]').get().forEach((element) => emojify.run(element));
|
||||
}
|
||||
|
||||
const emojifyPage = new AutoloadPage('emojifyPage', () => {
|
||||
emojify.setConfig({
|
||||
img_dir: `${UiContext.cdn_prefix}img/emoji`,
|
||||
});
|
||||
runEmojify($('body'));
|
||||
$(document).on('vjContentNew', (e) => runEmojify($(e.target)));
|
||||
});
|
||||
|
||||
export default emojifyPage;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { isBelow } from 'vj/utils/mediaQuery';
|
||||
import { slideUp, slideDown } from 'vj/utils/slide';
|
||||
import responsiveCutoff from 'vj/breakpoints.json';
|
||||
|
||||
const footerPage = new AutoloadPage('footerPage', () => {
|
||||
if ($('.footer').length === 0) {
|
||||
return;
|
||||
}
|
||||
$('.footer__category.expandable > h1').click(async (ev) => {
|
||||
if (!isBelow(responsiveCutoff.mobile)) {
|
||||
return;
|
||||
}
|
||||
const $category = $(ev.currentTarget).closest('.footer__category');
|
||||
const $list = $category.find('.footer__category__expander');
|
||||
if ($category.hasClass('animating')) {
|
||||
return;
|
||||
}
|
||||
$category.addClass('animating');
|
||||
if ($category.hasClass('expanded')) {
|
||||
$category.removeClass('expanded');
|
||||
await slideUp($list, 300, { opacity: 1 }, { opacity: 0 });
|
||||
} else {
|
||||
$category.addClass('expanded');
|
||||
await slideDown($list, 300, { opacity: 0 }, { opacity: 1 });
|
||||
}
|
||||
$category.removeClass('animating');
|
||||
});
|
||||
});
|
||||
|
||||
export default footerPage;
|
||||
@@ -0,0 +1,94 @@
|
||||
$footer-extra-link-gap = 20px
|
||||
|
||||
.footer
|
||||
background: $footer-bg-color
|
||||
color: $text-1-color
|
||||
|
||||
h1
|
||||
font-size: rem($font-size-title)
|
||||
|
||||
+mobile()
|
||||
padding: rem($grid-padding)
|
||||
cursor: pointer
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
.footer__category
|
||||
padding-top: rem(30px)
|
||||
padding-bottom: rem(50px)
|
||||
|
||||
+mobile()
|
||||
padding-top: rem($grid-padding)
|
||||
padding-bottom: rem($grid-padding)
|
||||
border-bottom: 1px solid darken($footer-bg-color, 5%)
|
||||
|
||||
&.expandable
|
||||
padding: 0
|
||||
|
||||
.expand-icon
|
||||
display: none
|
||||
|
||||
+mobile()
|
||||
display: inline-block
|
||||
|
||||
&.expanded .expand-icon
|
||||
transform: rotate(180deg)
|
||||
|
||||
.footer__category__list
|
||||
margin-top: rem(10px)
|
||||
margin-bottom: rem(10px)
|
||||
font-size: rem($font-size-small)
|
||||
line-height: 2
|
||||
|
||||
+mobile()
|
||||
margin-top: 0
|
||||
padding-left: rem($grid-padding)
|
||||
padding-right: rem($grid-padding)
|
||||
|
||||
.footer__category__expander
|
||||
+mobile()
|
||||
display: none
|
||||
|
||||
+above(rupture.mobile-cutoff)
|
||||
display: block !important
|
||||
|
||||
.footer__extra-left
|
||||
float: left
|
||||
|
||||
.footer__extra-right
|
||||
float: right
|
||||
|
||||
.footer__extra-link
|
||||
padding: rem(20px 0)
|
||||
font-size: rem($font-size-small)
|
||||
|
||||
.footer__extra-link-item
|
||||
float: left
|
||||
line-height: 2
|
||||
|
||||
.footer__extra-left .footer__extra-link-item
|
||||
margin-right: rem($footer-extra-link-gap)
|
||||
|
||||
.footer__extra-right .footer__extra-link-item
|
||||
margin-left: rem($footer-extra-link-gap)
|
||||
|
||||
.layout--immersive
|
||||
.footer
|
||||
background: none
|
||||
color: #FFF
|
||||
|
||||
a, a:visited, a:active
|
||||
color: #FFF
|
||||
|
||||
a:hover
|
||||
color: #FFF
|
||||
|
||||
+mobile()
|
||||
.footer__extra-left, .footer__extra-right
|
||||
float: none
|
||||
display: block
|
||||
|
||||
.footer__extra-link-item
|
||||
float: none
|
||||
display: inline-block
|
||||
margin-left: 0
|
||||
margin-right: $footer-extra-link-gap
|
||||
@@ -0,0 +1,82 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.button
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: rem($form-control-height - 2)
|
||||
background-color: lighten($default-color, 50%)
|
||||
border: 1px solid $default-color
|
||||
height: rem($form-control-height)
|
||||
padding: rem(0 25px)
|
||||
display: inline-block
|
||||
outline: 0
|
||||
margin: 0 0 1rem
|
||||
transition: background-color .2s, box-shadow .5s
|
||||
transition-timing-function: ease-out-sine
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
|
||||
&, &:visited, &:hover
|
||||
&, .typo &
|
||||
color: $text-1-color
|
||||
|
||||
&:hover, &:focus
|
||||
text-decoration: none
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover, &:focus
|
||||
background-color: darken($default-color, 5%)
|
||||
border-color: darken($default-color, 5%)
|
||||
box-shadow: 0 4px 15px rgba(darken($default-color, 10%), 0.5)
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-color
|
||||
|
||||
&.expanded
|
||||
padding-left: 0
|
||||
padding-right: 0
|
||||
text-align: center
|
||||
width: 100%
|
||||
|
||||
&.rounded
|
||||
border-radius: rem($form-control-height / 2)
|
||||
|
||||
&.primary
|
||||
border-color: $primary-color
|
||||
background-color: $primary-color
|
||||
|
||||
&, &:visited, &:hover
|
||||
&, .typo &
|
||||
color: #FFF
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover
|
||||
background-color: lighten(saturate($primary-color, 30%), 10%)
|
||||
border-color: saturate($primary-color, 70%)
|
||||
box-shadow: 0 4px 15px rgba(saturate($primary-color, 70%), 0.3)
|
||||
|
||||
&.disabled
|
||||
cursor: default
|
||||
opacity: 0.5
|
||||
|
||||
&.compact
|
||||
margin-bottom: 0
|
||||
height: rem($compact-control-height)
|
||||
line-height: rem($compact-control-height - 2)
|
||||
padding: rem(0 15px)
|
||||
|
||||
.button.inverse
|
||||
line-height: rem($form-control-height - 4)
|
||||
border: 2px solid #FFF
|
||||
background: none
|
||||
|
||||
&, &:visited
|
||||
&, .typo &
|
||||
color: #FFF
|
||||
|
||||
&:not(.disabled)
|
||||
&:hover, &:focus
|
||||
border-color: $immersive-primary-color
|
||||
background: none
|
||||
box-shadow: none
|
||||
&, .typo &
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,90 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
$checkbox-size = 16px
|
||||
|
||||
.checkbox
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.4
|
||||
border: 0
|
||||
display: inline-block
|
||||
vertical-align: middle
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
color: lighten($text-1-color, 30%)
|
||||
margin: $input-margin
|
||||
padding: ((rem($form-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
|
||||
&:hover
|
||||
color: $text-1-color
|
||||
|
||||
input
|
||||
appearance: none
|
||||
border: 1px solid #D0D0D0
|
||||
display: inline-block
|
||||
border-radius: 1px
|
||||
vertical-align: top
|
||||
width: rem($checkbox-size)
|
||||
height: rem($checkbox-size)
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
position: relative
|
||||
outline: 0
|
||||
transition: background .1s linear
|
||||
background: none
|
||||
cursor: pointer
|
||||
|
||||
&:after
|
||||
content: ''
|
||||
width: rem(10px)
|
||||
height: rem(6px)
|
||||
position: absolute
|
||||
top: rem(3px)
|
||||
left: rem(2px)
|
||||
border: 2px solid #FFF
|
||||
border-top: none
|
||||
border-right: none
|
||||
background: transparent
|
||||
transform: rotate(-65deg) scale(0.5)
|
||||
opacity: 0
|
||||
|
||||
&:enabled:hover
|
||||
border-color: rgba($secondary-color, 0.7)
|
||||
|
||||
&:checked
|
||||
background: $secondary-color
|
||||
border-color: $secondary-color
|
||||
|
||||
&:checked:after
|
||||
opacity: 1
|
||||
transform: rotate(-45deg)
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
&.compact
|
||||
margin-bottom: 0
|
||||
line-height: 1.2
|
||||
padding: ((rem($compact-control-height) - (@line-height * @font-size)) / 2) 0
|
||||
|
||||
input
|
||||
margin: ((@line-height * @font-size - rem($checkbox-size)) / 2) 0
|
||||
margin-right: rem(5px)
|
||||
|
||||
label.checkbox.inverse
|
||||
color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
input
|
||||
color: $immersive-text-color
|
||||
border-color: rgba($immersive-text-color, 0.4)
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($immersive-text-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $immersive-primary-color
|
||||
|
||||
&.focus
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const formPage = new AutoloadPage('formPage', () => {
|
||||
$(document).on('vjFormDisableUpdate', 'input, select, textarea', (ev) => {
|
||||
const $input = $(ev.currentTarget);
|
||||
const $formItem = $input.closest('.form__item');
|
||||
if ($input.prop('disabled')) {
|
||||
$formItem.addClass('is--disabled');
|
||||
} else {
|
||||
$formItem.removeClass('is--disabled');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default formPage;
|
||||
@@ -0,0 +1,199 @@
|
||||
// Extracted from Foundation framework
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea
|
||||
color: inherit
|
||||
/* 1 */
|
||||
font: inherit
|
||||
/* 2 */
|
||||
margin: 0
|
||||
/* 3 */
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
button
|
||||
overflow: visible
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
button,
|
||||
select
|
||||
text-transform: none
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
button,
|
||||
html input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"]
|
||||
appearance: button
|
||||
/* 2 */
|
||||
cursor: pointer
|
||||
/* 3 */
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
button[disabled],
|
||||
html input[disabled]
|
||||
cursor: not-allowed
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner
|
||||
border: 0
|
||||
padding: 0
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
input
|
||||
line-height: normal
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
input[type="checkbox"],
|
||||
input[type="radio"]
|
||||
box-sizing: border-box
|
||||
/* 1 */
|
||||
padding: 0
|
||||
/* 2 */
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button
|
||||
height: auto
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
|
||||
*/
|
||||
input[type="search"]
|
||||
appearance: textfield
|
||||
/* 1 */
|
||||
box-sizing: content-box
|
||||
/* 2 */
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration
|
||||
-webkit-appearance: none
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
* [NOTE] We don't enable this ruleset in Foundation, because we want the <fieldset> element to have plain styling.
|
||||
*/
|
||||
/* fieldset
|
||||
border: 1px solid #c0c0c0
|
||||
margin: 0 2px
|
||||
padding: 0.35em 0.625em 0.75em
|
||||
*/
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
legend
|
||||
border: 0
|
||||
/* 1 */
|
||||
padding: 0
|
||||
/* 2 */
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
textarea
|
||||
overflow: auto
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
optgroup
|
||||
font-weight: bold
|
||||
|
||||
.form--inline
|
||||
display: inline
|
||||
|
||||
textarea
|
||||
height: auto
|
||||
border-radius: 0px
|
||||
resize: vertical
|
||||
|
||||
select
|
||||
width: 100%
|
||||
border-radius: 0px
|
||||
|
||||
button
|
||||
appearance: none
|
||||
background: transparent
|
||||
padding: 0
|
||||
border: 0
|
||||
border-radius: 0px
|
||||
line-height: 1
|
||||
|
||||
[data-whatinput='mouse'] button
|
||||
outline: 0
|
||||
|
||||
label
|
||||
display: block
|
||||
margin: 0
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.8
|
||||
color: $label-color
|
||||
|
||||
input, textarea, select
|
||||
display: block
|
||||
|
||||
.help-text
|
||||
margin-top: -0.5rem
|
||||
margin-bottom: 1rem
|
||||
font-size: rem($font-size-small)
|
||||
color: $help-text-color
|
||||
|
||||
.form__item
|
||||
transition: opacity .1s linear
|
||||
|
||||
&.is--disabled
|
||||
opacity: 0.3
|
||||
|
||||
:disabled
|
||||
opacity: 1
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const lastSelectionByGroup = {};
|
||||
|
||||
function isInGroup(group) {
|
||||
return (idx, element) => element.getAttribute('data-checkbox-group') === group;
|
||||
}
|
||||
|
||||
function onRangeRoleCheckboxClick(ev) {
|
||||
const $current = $(ev.currentTarget);
|
||||
const targetGroup = $current.attr('data-checkbox-group');
|
||||
const $targets = $('[type="checkbox"]')
|
||||
.filter(isInGroup(targetGroup))
|
||||
.filter(':visible')
|
||||
.filter(':not(:disabled)');
|
||||
if (ev.shiftKey && lastSelectionByGroup[targetGroup]) {
|
||||
const destCheck = lastSelectionByGroup[targetGroup].prop('checked');
|
||||
const from = $targets.index(lastSelectionByGroup[targetGroup]);
|
||||
const to = $targets.index($current);
|
||||
const start = Math.min(from, to);
|
||||
const end = Math.max(from, to) + 1;
|
||||
$targets
|
||||
.slice(start, end)
|
||||
.prop('checked', destCheck);
|
||||
}
|
||||
lastSelectionByGroup[targetGroup] = $current;
|
||||
}
|
||||
|
||||
function onToggleRoleCheckboxClick(ev) {
|
||||
const $current = $(ev.currentTarget);
|
||||
const targetGroup = $current.attr('data-checkbox-toggle');
|
||||
const $targets = $('[type="checkbox"]')
|
||||
.filter(isInGroup(targetGroup))
|
||||
.filter(':visible')
|
||||
.filter(':not(:disabled)');
|
||||
const destCheck = $current.prop('checked');
|
||||
$targets.prop('checked', destCheck);
|
||||
}
|
||||
|
||||
const multiSelectCheckboxPage = new AutoloadPage('multiSelectCheckboxPage', () => {
|
||||
$(document).on('click', '[data-checkbox-range]', onRangeRoleCheckboxClick);
|
||||
$(document).on('click', '[data-checkbox-toggle]', onToggleRoleCheckboxClick);
|
||||
});
|
||||
|
||||
export default multiSelectCheckboxPage;
|
||||
@@ -0,0 +1,62 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
$radiobox-size = 16px
|
||||
|
||||
.radiobox
|
||||
display: block
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.4
|
||||
border: 0
|
||||
cursor: pointer
|
||||
user-select: none
|
||||
color: lighten($text-1-color, 30%)
|
||||
margin: $input-multi-line-margin
|
||||
|
||||
&:hover
|
||||
color: $text-1-color
|
||||
|
||||
input
|
||||
display: inline-block
|
||||
|
||||
.radiobox-container
|
||||
margin: $input-margin
|
||||
|
||||
// Image Radio Box
|
||||
|
||||
.radiobox.with-image
|
||||
margin: $input-image-radio-margin
|
||||
|
||||
.radiobox__image-container
|
||||
cursor: pointer
|
||||
border: 2px solid #DDD
|
||||
transition: border-color .1s linear, opacity .1s linear
|
||||
padding: 1px
|
||||
overflow: hidden
|
||||
position: relative
|
||||
opacity: 0.7
|
||||
|
||||
&:hover
|
||||
border: 2px solid $primary-color
|
||||
opacity: 1
|
||||
|
||||
&:after
|
||||
position: absolute
|
||||
width: rem(20px)
|
||||
height: rem(20px)
|
||||
right: rem(-10px)
|
||||
top: rem(-10px)
|
||||
transform: translateX(10px) translateY(-10px) rotate(45deg)
|
||||
background: $highlight-color
|
||||
content: ''
|
||||
transition: transform .1s ease-out-cubic
|
||||
|
||||
input
|
||||
visibility: hidden
|
||||
position: absolute
|
||||
|
||||
&:checked + .radiobox__image-container
|
||||
border: 2px solid $highlight-color
|
||||
opacity: 1
|
||||
|
||||
&:after
|
||||
transform: rotate(45deg)
|
||||
@@ -0,0 +1,29 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.select
|
||||
form-styles()
|
||||
padding-right: 1.1rem
|
||||
background-color: $input-background-color
|
||||
background-image: url('~vj/misc/icons/' + $icon-expand_more-file)
|
||||
background-size: 16px 16px
|
||||
background-position: right -1rem center
|
||||
background-origin: content-box
|
||||
background-repeat: no-repeat
|
||||
outline: $input-outline
|
||||
transition: outline-color .2s, border-color .2s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-border-color
|
||||
outline: $input-focus-outline
|
||||
outline-offset: 0
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
.select-container.compact .select, .select.compact
|
||||
margin-bottom: 0
|
||||
height: rem($compact-control-height)
|
||||
line-height: rem($compact-control-height - 2)
|
||||
padding-top: 0
|
||||
padding-bottom: 0
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const textboxPage = new AutoloadPage('textboxPage', () => {
|
||||
$(document).on('focusin', '.textbox.material input', (ev) => {
|
||||
$(ev.currentTarget).parent().addClass('focus');
|
||||
});
|
||||
|
||||
$(document).on('focusout', '.textbox.material input', (ev) => {
|
||||
$(ev.currentTarget).parent().removeClass('focus');
|
||||
});
|
||||
|
||||
const $focusElement = $(document.activeElement);
|
||||
if ($focusElement.prop('tagName') === 'INPUT'
|
||||
&& $focusElement.parent().is('.textbox.material')
|
||||
) {
|
||||
$focusElement.focusin();
|
||||
}
|
||||
});
|
||||
|
||||
export default textboxPage;
|
||||
@@ -0,0 +1,72 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
// Normal Textbox
|
||||
input.textbox, textarea.textbox
|
||||
form-styles()
|
||||
background-color: $input-background-color
|
||||
outline: $input-outline
|
||||
transition: outline-color .2s, border-color .2s
|
||||
transition-timing-function: ease-out-cubic
|
||||
|
||||
&:focus
|
||||
border-color: $input-focus-border-color
|
||||
outline: $input-focus-outline
|
||||
outline-offset: 0
|
||||
|
||||
// Overwrite, for IE compatibility
|
||||
&:read-only
|
||||
outline: 0
|
||||
|
||||
&:disabled
|
||||
opacity: 0.5
|
||||
|
||||
textarea.textbox
|
||||
height: rem(200px)
|
||||
|
||||
.data-table
|
||||
input.textbox, textarea.textbox
|
||||
margin-bottom: 0
|
||||
|
||||
// Material Textbox
|
||||
label.textbox.material
|
||||
input
|
||||
form-styles()
|
||||
border: 0
|
||||
background: none
|
||||
outline: 0
|
||||
padding-left: 0
|
||||
padding-right: 0
|
||||
color: $text-1-color
|
||||
border-bottom: 2px solid rgba($text-1-color, 0.4)
|
||||
transition: border-color .2s ease-out-cubic
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($text-1-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($text-1-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $input-focus-border-color
|
||||
|
||||
&.focus
|
||||
color: $input-focus-border-color
|
||||
|
||||
label.textbox.material.inverse
|
||||
color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
input
|
||||
color: $immersive-text-color
|
||||
border-color: rgba($immersive-text-color, 0.4)
|
||||
|
||||
&:disabled, &:read-only
|
||||
color: rgba($immersive-text-color, 0.6)
|
||||
|
||||
&:enabled:read-write:hover
|
||||
border-color: rgba($immersive-text-color, 0.8)
|
||||
|
||||
&:enabled:read-write:focus
|
||||
border-color: $immersive-primary-color
|
||||
|
||||
&.focus
|
||||
color: $immersive-primary-color
|
||||
@@ -0,0 +1,14 @@
|
||||
form-styles()
|
||||
appearance: none
|
||||
display: block
|
||||
width: 100%
|
||||
font-size: rem($font-size-secondary)
|
||||
margin: $input-margin
|
||||
height: rem($form-control-height)
|
||||
line-height: 1.2
|
||||
padding: rem(5px)
|
||||
border: $input-border
|
||||
|
||||
&.inline
|
||||
display: inline-block
|
||||
width: auto
|
||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,176 @@
|
||||
$header-gap-mini = ($header-bg-height-mini - $nav-item-height - $header-logo-height) / 2
|
||||
$header-gap-mini-domain = ($header-bg-height-mini - $nav-item-height - $header-logo-domain-lh - $header-logo-system-lh) / 2
|
||||
|
||||
.global-notification
|
||||
padding: rem(20px 0)
|
||||
background: #FFF
|
||||
|
||||
&.warning
|
||||
background: #ffe1a6
|
||||
|
||||
.header__options
|
||||
background-image: url(./header-background.png)
|
||||
background-image: url(./header-background@2x.png)
|
||||
|
||||
.header
|
||||
position: relative
|
||||
height: rem($header-bg-height)
|
||||
background: #5eafca no-repeat center top
|
||||
background-size: cover
|
||||
transition: height .5s
|
||||
transition-timing-function: ease-out-cubic
|
||||
overflow: hidden
|
||||
+retina()
|
||||
background-size: rem($header-bg-width $header-bg-height)
|
||||
|
||||
&.mini
|
||||
height: rem($header-bg-height-mini)
|
||||
|
||||
+mobile()
|
||||
background: #FFF
|
||||
height: auto !important
|
||||
.header__content
|
||||
display: none
|
||||
|
||||
.header__logo__options
|
||||
background-image: url(./header-logo.png)
|
||||
background-image: url(./header-logo@2x.png)
|
||||
background-image: url(./header-logo-summer.png)
|
||||
background-image: url(./header-logo-summer@2x.png)
|
||||
|
||||
.header__logo
|
||||
display: block
|
||||
width: rem($header-logo-width-real)
|
||||
height: rem($header-logo-height + $header-gap-mini + $nav-item-height)
|
||||
background: no-repeat
|
||||
background-size: rem($header-logo-width-real) rem($header-logo-height-real)
|
||||
background-position: 0 rem($header-gap-mini + $nav-item-height - ($header-logo-height-real - $header-logo-height))
|
||||
transition: transform .5s, opacity .5s
|
||||
transition-timing-function: ease-out-cubic
|
||||
transform-origin: 0 0
|
||||
opacity: 1
|
||||
+mobile()
|
||||
display: inline-block
|
||||
|
||||
.header__logo--domain
|
||||
font-size: rem(40px)
|
||||
margin-top: rem($header-gap-mini-domain + $nav-item-height)
|
||||
height: rem($header-logo-domain-lh)
|
||||
line-height: rem($header-logo-domain-lh)
|
||||
|
||||
a
|
||||
text-shadow: 1px 1px 5px rgba(#000, 0.4)
|
||||
|
||||
&, &:hover, &:visited, &:link
|
||||
color: #FFF
|
||||
text-decoration: none
|
||||
|
||||
.header__logo--system
|
||||
line-height: rem($header-logo-system-lh)
|
||||
color: #FFF
|
||||
font-size: rem($font-size-small)
|
||||
|
||||
a
|
||||
opacity: 0.8
|
||||
|
||||
&, &:hover, &:visited, &:link
|
||||
text-decoration: none
|
||||
color: #FFF
|
||||
|
||||
&:hover
|
||||
opacity: 1
|
||||
text-decoration: underline
|
||||
|
||||
.header--collapsed
|
||||
|
||||
#panel
|
||||
padding-top: $header-bg-height
|
||||
|
||||
.header
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 0
|
||||
|
||||
.header__logo
|
||||
transform: scale(0)
|
||||
opacity: 0
|
||||
|
||||
.location
|
||||
position: absolute
|
||||
left: 0
|
||||
bottom: 0
|
||||
width: 100%
|
||||
height: rem($header-layer-height)
|
||||
background: rgba(#000, 0.5)
|
||||
|
||||
+mobile()
|
||||
position: relative
|
||||
height: auto
|
||||
|
||||
.header__hamburger
|
||||
display: none
|
||||
|
||||
+mobile()
|
||||
display: block
|
||||
|
||||
.header.mini .location
|
||||
display: none
|
||||
|
||||
.location-path
|
||||
color: rgba(#FFF, 0.3)
|
||||
line-height: rem(30px)
|
||||
margin-top: rem(10px)
|
||||
a, a:visited
|
||||
color: rgba(#FFF, 0.6)
|
||||
a:hover
|
||||
color: rgba(#FFF, 0.9)
|
||||
|
||||
.location-current
|
||||
color: #9ad4f1
|
||||
line-height: rem(40px)
|
||||
white-space: nowrap
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
+mobile()
|
||||
color: #91fcff
|
||||
font-size: 1.7rem
|
||||
white-space: normal
|
||||
|
||||
.header--mobile
|
||||
display: none
|
||||
|
||||
.header--mobile, .location
|
||||
+mobile()
|
||||
display: block
|
||||
background: #56758f
|
||||
color: #FFF
|
||||
line-height: rem($nav-item-height)
|
||||
font-size: rem($font-size-title)
|
||||
padding: rem(10px 0)
|
||||
|
||||
a, a:visited, a:hover
|
||||
color: #FFF
|
||||
|
||||
.header--mobile__domain.is--system
|
||||
display: block
|
||||
width: $nav-logo-width // no rem
|
||||
height: $nav-logo-height // no rem
|
||||
margin-top: rem(10px)
|
||||
+retina()
|
||||
background-size: $nav-logo-width $nav-logo-height // no rem
|
||||
|
||||
.header--mobile__system
|
||||
margin-left: rem(10px)
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
&, &:hover, &:visited
|
||||
color: $supplementary-color
|
||||
|
||||
.header__hamburger
|
||||
background: none
|
||||
border: 0
|
||||
border-radius: 0
|
||||
outline: 0
|
||||
@@ -0,0 +1,12 @@
|
||||
@css {
|
||||
span.cm-comment { color: #008000; }
|
||||
span.cm-keyword, span.cm-variable-3 { line-height: 1em; color: #00f; }
|
||||
span.cm-string { color: #a31515; }
|
||||
span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; }
|
||||
span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; }
|
||||
span.cm-variable { color: black; }
|
||||
span.cm-meta { color: #2b91af; }
|
||||
span.cm-link { color: #3a3; }
|
||||
.CodeMirror-activeline-background { background: #e8f2ff; }
|
||||
.CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const highlighterPage = new AutoloadPage('highlighterPage', () => {
|
||||
import('./prismjs').then((module) => {
|
||||
const prismjs = module.default;
|
||||
function runHighlight($container) {
|
||||
prismjs.highlightBlocks($container);
|
||||
}
|
||||
runHighlight($('body'));
|
||||
$(document).on('vjContentNew', (e) => runHighlight($(e.target)));
|
||||
});
|
||||
});
|
||||
|
||||
export default highlighterPage;
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
|
||||
VS theme by Andrew Lock
|
||||
|
||||
https://github.com/PrismJS/prism-themes/blob/master/themes/prism-vs.css
|
||||
|
||||
*/
|
||||
|
||||
pre, code
|
||||
background: $syntax-hl-bg-color
|
||||
|
||||
pre > code
|
||||
background: none
|
||||
line-height: 1.5
|
||||
|
||||
pre
|
||||
position: relative
|
||||
padding: rem(5px)
|
||||
|
||||
code
|
||||
color: $code-color
|
||||
font-size: rem($font-size-secondary)
|
||||
|
||||
pre > code
|
||||
color: black
|
||||
text-align: left
|
||||
white-space: pre-wrap
|
||||
word-spacing: normal
|
||||
word-break: normal
|
||||
tab-size: 4
|
||||
hyphens: none
|
||||
|
||||
[data-syntax-hl-show-line-number] pre.line-numbers > code
|
||||
white-space: pre
|
||||
|
||||
.syntax-hl
|
||||
.token
|
||||
&.comment, &.prolog, &.doctype, &.cdata
|
||||
color: #008000
|
||||
font-style: italic
|
||||
&.namespace
|
||||
opacity: 0.7
|
||||
&.string
|
||||
color: #a31515
|
||||
&.punctuation, &.operator
|
||||
color: #393A34
|
||||
&.url, &.symbol, &.number, &.boolean, &.inserted
|
||||
color: #36acaa
|
||||
&.atrule, &.keyword, &.attr-value, .language-autohotkey&.selector, .language-json&.boolean, .language-json&.number
|
||||
color: #0000ff
|
||||
&.function
|
||||
color: #393A34
|
||||
&.deleted, .language-autohotkey&.tag
|
||||
color: #9a050f
|
||||
&.selector, .language-autohotkey&.keyword
|
||||
color: #00009f
|
||||
&.important, &.bold
|
||||
font-weight: bold
|
||||
&.italic
|
||||
font-style: italic
|
||||
&.class-name, .language-json&.property
|
||||
color: #2B91AF
|
||||
&.tag, &.selector
|
||||
color: #800000
|
||||
&.attr-name, &.property, &.regex, &.entity
|
||||
color: #ff0000
|
||||
&.directive.tag .tag
|
||||
background: #ffff00
|
||||
color: #393A34
|
||||
&.macro
|
||||
&, .token
|
||||
color: #2b91af
|
||||
&.builtin
|
||||
color: #077
|
||||
|
||||
.toolbar
|
||||
position: absolute
|
||||
white-space: no-wrap
|
||||
right: 0
|
||||
top: 0
|
||||
padding: rem(5px)
|
||||
background: rgba($syntax-hl-bg-color, 0.8)
|
||||
font-family: $primary-font-family
|
||||
font-size: rem($font-size-small)
|
||||
opacity: 0
|
||||
pointer-events: none
|
||||
transition: opacity .1s linear
|
||||
user-select: none
|
||||
|
||||
&:hover .toolbar
|
||||
opacity: 1
|
||||
pointer-events: all
|
||||
|
||||
[data-syntax-hl-show-line-number]
|
||||
pre
|
||||
padding-left: 3.8em
|
||||
counter-reset: linenumber
|
||||
|
||||
pre > code
|
||||
position: relative
|
||||
|
||||
.line-numbers-rows
|
||||
position: absolute
|
||||
pointer-events: none
|
||||
top: 0
|
||||
font-size: 100%
|
||||
left: -3.8em
|
||||
width: 3em
|
||||
letter-spacing: -1px
|
||||
user-select: none
|
||||
|
||||
span
|
||||
pointer-events: none
|
||||
display: block
|
||||
counter-increment: linenumber
|
||||
|
||||
&:before
|
||||
content: counter(linenumber)
|
||||
color: #ddd
|
||||
display: block
|
||||
padding-right: 0.8em
|
||||
text-align: right
|
||||
@@ -0,0 +1,15 @@
|
||||
// The following content is extracted from:
|
||||
// https://github.com/codemirror/CodeMirror/blob/master/mode/meta.js
|
||||
|
||||
export default [
|
||||
{ name: 'C', ext: ['c', 'h'] },
|
||||
{ name: 'C++', ext: ['cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx'] },
|
||||
{ name: 'Go', ext: ['go'] },
|
||||
{ name: 'Haskell', ext: ['hs'] },
|
||||
{ name: 'Java', ext: ['java'] },
|
||||
{ name: 'JavaScript', ext: ['js'] },
|
||||
{ name: 'Pascal', ext: ['p', 'pas'] },
|
||||
{ name: 'PHP', ext: ['php', 'php3', 'php4', 'php5', 'php7', 'phtml'] },
|
||||
{ name: 'Python', ext: ['BUILD', 'bzl', 'py', 'pyw'] },
|
||||
{ name: 'Rust', ext: ['rs'] },
|
||||
];
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
|
||||
To add a new language to highlight:
|
||||
1. Add language in babel in package.json
|
||||
2. Add new import statement in `components/cmeditor/vjcmeditor.js`
|
||||
3. Add new import statement in `components/scratchpad/ScratchpadEditorContainer.js`
|
||||
4. Add new meta data in `components/highlighter/meta.js`
|
||||
|
||||
*/
|
||||
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import Clipboard from 'clipboard';
|
||||
import Notification from 'vj/components/notification';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
import languageMeta from './meta';
|
||||
|
||||
const languageExtMap = {};
|
||||
|
||||
// Map possible language names to Prism language name
|
||||
languageMeta.forEach((meta) => {
|
||||
for (let i = 0; i < meta.ext.length; ++i) {
|
||||
if (Prism.languages[meta.ext[i]] !== undefined) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
meta.target = meta.ext[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
meta.ext.forEach((ext) => {
|
||||
languageExtMap[ext] = meta.target;
|
||||
});
|
||||
});
|
||||
|
||||
// Copy to Clipboard
|
||||
try {
|
||||
Prism.plugins.toolbar.registerButton('copy-to-clipboard', (env) => {
|
||||
const linkCopy = document.createElement('a');
|
||||
linkCopy.href = 'javascript:;'; // eslint-disable-line no-script-url
|
||||
linkCopy.textContent = 'Copy';
|
||||
const clip = new Clipboard(linkCopy, { text: () => env.code });
|
||||
clip.on('success', () => {
|
||||
Notification.success(i18n('Content copied to clipboard!'), 1000);
|
||||
});
|
||||
clip.on('error', () => {
|
||||
Notification.error(i18n('Copy failed :('));
|
||||
});
|
||||
return linkCopy;
|
||||
});
|
||||
} catch (e) {
|
||||
// snowpack
|
||||
}
|
||||
|
||||
const prismjsApiWrap = {
|
||||
highlightBlocks: ($dom) => {
|
||||
$dom.find('pre code').get().forEach((code) => {
|
||||
const $pre = $(code).parent();
|
||||
$pre.addClass('syntax-hl');
|
||||
if ($pre.closest('[data-syntax-hl-show-line-number]')) {
|
||||
$pre.addClass('line-numbers');
|
||||
}
|
||||
// try to map the language name
|
||||
const language = $(code).attr('class');
|
||||
const m = (language || '').trim().match(/^language-(.+)$/);
|
||||
if (m && m[1]) {
|
||||
const languageName = m[1].toLowerCase();
|
||||
if (languageExtMap[languageName]) {
|
||||
$(code).attr('class', `language-${languageExtMap[languageName]}`);
|
||||
}
|
||||
}
|
||||
Prism.highlightElement(code);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default prismjsApiWrap;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import request from 'vj/utils/request';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
|
||||
const hitokotoPage = new AutoloadPage('hitokotoPage', () => {
|
||||
function getHitokoto($containers) {
|
||||
$containers.get().forEach((container) => {
|
||||
request.get('https://v1.hitokoto.cn?c=a&c=b&c=c&c=d&c=e&c=f')
|
||||
.then((hitokoto) => {
|
||||
const dom = $(tpl`<p>${hitokoto.hitokoto}</p>`);
|
||||
dom.appendTo(container);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
const dom = $(tpl`<p>${i18n('Cannot get hitokoto.')}</p>`);
|
||||
dom.appendTo(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
if ($('[name="hitokoto"]')) getHitokoto($('[name="hitokoto"]'));
|
||||
$(document).on('vjContentNew', (e) => {
|
||||
const elem = $(e.target).find('[name="hitokoto"]');
|
||||
if (elem.get) getHitokoto(elem);
|
||||
});
|
||||
});
|
||||
|
||||
export default hitokotoPage;
|
||||
@@ -0,0 +1,80 @@
|
||||
import _ from 'lodash';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const KEY_MAP = {
|
||||
10: 'enter',
|
||||
13: 'enter',
|
||||
27: 'esc',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
112: 'f1',
|
||||
113: 'f2',
|
||||
114: 'f3',
|
||||
115: 'f4',
|
||||
116: 'f5',
|
||||
117: 'f6',
|
||||
118: 'f7',
|
||||
119: 'f8',
|
||||
120: 'f9',
|
||||
121: 'f10',
|
||||
122: 'f11',
|
||||
123: 'f12',
|
||||
};
|
||||
|
||||
function isHotkeyMatch(sortedHotkeyArr, hotkeyStr) {
|
||||
const hotkeyDefined = hotkeyStr.toLowerCase().split('+');
|
||||
return _.isEqual(sortedHotkeyArr, hotkeyDefined.sort());
|
||||
}
|
||||
|
||||
function testElementHotkey(hotkey, $element, attr) {
|
||||
if (!$element.is(':visible')) {
|
||||
return;
|
||||
}
|
||||
String($element.attr(attr))
|
||||
.split(',')
|
||||
.forEach((singleDef) => {
|
||||
const [defStr, trigger] = singleDef.split(':');
|
||||
if (isHotkeyMatch(hotkey, defStr)) {
|
||||
switch (trigger) {
|
||||
case 'submit':
|
||||
$element.closest('form').trigger('submit');
|
||||
break;
|
||||
case undefined:
|
||||
$element.trigger('click');
|
||||
break;
|
||||
default:
|
||||
$element.trigger(trigger);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const hotkeyPage = new AutoloadPage('hotkeyPage', () => {
|
||||
$(document).on('keydown', (ev) => {
|
||||
const hotkey = ['alt', 'ctrl', 'shift'].filter((modifyKey) => ev[`${modifyKey}Key`]);
|
||||
if (ev.metaKey && !ev.ctrlKey) {
|
||||
hotkey.push('ctrl');
|
||||
}
|
||||
if (KEY_MAP[ev.which] !== undefined) {
|
||||
hotkey.push(KEY_MAP[ev.which]);
|
||||
} else {
|
||||
hotkey.push(String.fromCharCode(ev.which).toLowerCase());
|
||||
}
|
||||
hotkey.sort();
|
||||
|
||||
// Find all global hotkeys
|
||||
$('[data-global-hotkey]').get().forEach((element) => {
|
||||
testElementHotkey(hotkey, $(element), 'data-global-hotkey');
|
||||
});
|
||||
|
||||
// Find all local hotkeys
|
||||
$(ev.target).parents('[data-hotkey]').get().forEach((element) => {
|
||||
testElementHotkey(hotkey, $(element), 'data-hotkey');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default hotkeyPage;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
|
||||
const katexPage = new AutoloadPage('katexPage', () => {
|
||||
import('katex/dist/contrib/auto-render.min.js').then(({ default: katex }) => {
|
||||
function runKatex($containers) {
|
||||
$containers.get().forEach((container) => katex(container));
|
||||
}
|
||||
runKatex($('.typo'));
|
||||
$(document).on('vjContentNew', (e) => runKatex($(e.target).find('.typo')));
|
||||
});
|
||||
});
|
||||
|
||||
export default katexPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
.katex
|
||||
font-size: 1.2em
|
||||
@@ -0,0 +1,29 @@
|
||||
.loader-container
|
||||
position: absolute
|
||||
left: 0
|
||||
width: 100%
|
||||
top: 0
|
||||
height: 100%
|
||||
|
||||
.loader
|
||||
text-indent: -9999em
|
||||
border: rem(5px) solid rgba($primary-color, .2)
|
||||
border-left: rem(5px) solid $primary-color
|
||||
animation: load8 1.1s infinite linear
|
||||
border-radius: 50%
|
||||
width: rem(50px)
|
||||
height: rem(50px)
|
||||
margin-left: rem(-25px)
|
||||
margin-top: rem(-25px)
|
||||
left: 50%
|
||||
top: 50%
|
||||
position: absolute
|
||||
|
||||
@keyframes load8
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import _ from 'lodash';
|
||||
import delay from 'vj/utils/delay';
|
||||
import Tooltip from 'vj/components/tooltip/Tooltip';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
|
||||
const MARKER_ID = `marker_${Math.floor(Math.random() * 0xFFFFFF).toString(16)}`;
|
||||
const MARKER_OFFSET = 20;
|
||||
const MARKER_MAX_DISTANCE = 60;
|
||||
|
||||
let markerInstance = null;
|
||||
|
||||
function distanceToRect(px, py, rect) {
|
||||
const cx = (rect.left + rect.right) / 2;
|
||||
const cy = (rect.top + rect.bottom) / 2;
|
||||
const dx = Math.max(Math.abs(px - cx) - (rect.right - rect.left) / 2, 0);
|
||||
const dy = Math.max(Math.abs(py - cy) - (rect.bottom - rect.top) / 2, 0);
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
class Marker {
|
||||
static exists() {
|
||||
return markerInstance && document.getElementById(MARKER_ID);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (Marker.exists()) {
|
||||
return markerInstance;
|
||||
}
|
||||
if (markerInstance) {
|
||||
markerInstance.destroy();
|
||||
}
|
||||
this.$dom = $(`
|
||||
<div class="marker" id="${MARKER_ID}"><div class="marker__toolbar">
|
||||
<div class="marker__action" data-color="#ffff00" data-tooltip="${i18n('Mark Yellow')}"><span class="marker__icon icon-yellow"></span></div>
|
||||
<div class="marker__action" data-color="#47ff6f" data-tooltip="${i18n('Mark Green')}"><span class="marker__icon icon-green"></span></div>
|
||||
<div class="marker__action" data-color="transparent" data-tooltip="${i18n('Clear Marks')}"><span class="icon icon-erase"></span></div>
|
||||
</div></div>
|
||||
`)
|
||||
.appendTo('body');
|
||||
this.$dom.find('.marker__toolbar').on('click', '.marker__action', this.onMarkerActionClick.bind(this));
|
||||
this.$dom.on('mousedown', this.onMarkerMouseDown.bind(this));
|
||||
this.isOpen = false;
|
||||
this.bindedHandlers = false;
|
||||
this._onKeyDown = this.onKeyDown.bind(this);
|
||||
this._onScroll = _.throttle(this.onScroll.bind(this), 50);
|
||||
this._onMouseDown = this.onMouseDown.bind(this);
|
||||
this._onMouseMove = _.throttle(this.onMouseMove.bind(this), 50);
|
||||
markerInstance = this;
|
||||
}
|
||||
|
||||
markSelection(color) {
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
this.$container.attr('contentEditable', true);
|
||||
if (range) {
|
||||
// recover ranges
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
if (!document.execCommand('HiliteColor', false, color)) {
|
||||
document.execCommand('BackColor', false, color);
|
||||
}
|
||||
this.$container.removeAttr('contentEditable');
|
||||
if (range) {
|
||||
// remove ranges after marking
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
|
||||
showAtPosition($container, x, y) {
|
||||
this.$container = $container;
|
||||
this.$dom.css({
|
||||
display: 'block',
|
||||
opacity: 0,
|
||||
});
|
||||
const rect = this.$dom[0].getBoundingClientRect();
|
||||
this.$dom.css({
|
||||
left: x + MARKER_OFFSET,
|
||||
top: y - (rect.bottom - rect.top) - MARKER_OFFSET,
|
||||
});
|
||||
this.isOpen = true;
|
||||
this.$dom.addClass('open');
|
||||
this.updateMarkerOpacity(x, y);
|
||||
this.bindEventHandlersForClosing();
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.isOpen = false;
|
||||
this.closeTooltipsImmediately();
|
||||
this.unbindEventHandlers();
|
||||
this.$dom
|
||||
.css({ opacity: 0 })
|
||||
.removeClass('open');
|
||||
await delay(200);
|
||||
if (!this.isOpen) {
|
||||
this.$dom.hide();
|
||||
}
|
||||
}
|
||||
|
||||
closeTooltipsImmediately() {
|
||||
const $elements = this.$dom.find('[data-tooltip]');
|
||||
$elements.get().forEach((element) => {
|
||||
const $el = $(element);
|
||||
const tooltipInstance = Tooltip.get($el);
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bindEventHandlersForClosing() {
|
||||
if (this.bindedHandlers) {
|
||||
return;
|
||||
}
|
||||
$(document).on('keydown', this._onKeyDown);
|
||||
$(window).on('scroll', this._onScroll);
|
||||
$(document).on('mousedown', this._onMouseDown);
|
||||
$(document).on('mousemove', this._onMouseMove);
|
||||
this.bindedHandlers = true;
|
||||
}
|
||||
|
||||
unbindEventHandlers() {
|
||||
if (!this.bindedHandlers) {
|
||||
return;
|
||||
}
|
||||
$(document).off('keydown', this._onKeyDown);
|
||||
$(window).off('scroll', this._onScroll);
|
||||
$(document).off('mousedown', this._onMouseDown);
|
||||
$(document).off('mousemove', this._onMouseMove);
|
||||
this.bindedHandlers = false;
|
||||
}
|
||||
|
||||
onMarkerActionClick(ev) {
|
||||
const color = $(ev.currentTarget).attr('data-color');
|
||||
this.markSelection(color);
|
||||
this.close();
|
||||
}
|
||||
|
||||
onMarkerMouseDown(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onKeyDown() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
onMouseMove(ev) {
|
||||
this.updateMarkerOpacity(ev.clientX, ev.clientY);
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
updateMarkerOpacity(x, y) {
|
||||
const markerRect = this.$dom[0].getBoundingClientRect();
|
||||
const distance = distanceToRect(x, y, markerRect);
|
||||
if (distance > MARKER_MAX_DISTANCE) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
this.$dom.css('opacity', 1 - (distance / MARKER_MAX_DISTANCE));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
this.$dom.remove();
|
||||
markerInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
const MarkerInterface = {
|
||||
close() {
|
||||
if (!markerInstance) {
|
||||
return;
|
||||
}
|
||||
markerInstance.close();
|
||||
},
|
||||
showAtPosition($container, x, y) {
|
||||
const marker = new Marker();
|
||||
marker.showAtPosition($container, x, y);
|
||||
},
|
||||
};
|
||||
|
||||
export default MarkerInterface;
|
||||
@@ -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;
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
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
|
||||
|
After Width: | Height: | Size: 76 KiB |