mirror of
https://github.com/flarum/framework.git
synced 2025-05-23 07:09:57 +08:00
Replace Ember app with Mithril app
This commit is contained in:
42
js/lib/component.js
Normal file
42
js/lib/component.js
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
|
||||
*/
|
||||
export default class Component {
|
||||
/**
|
||||
|
||||
*/
|
||||
constructor(props) {
|
||||
this.props = props || {};
|
||||
|
||||
this.element = m.prop();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
$(selector) {
|
||||
return selector ? $(this.element()).find(selector) : $(this.element());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
static component(props) {
|
||||
props = props || {};
|
||||
var view = function(component) {
|
||||
component.props = props;
|
||||
return component.view();
|
||||
};
|
||||
view.$original = this.prototype.view;
|
||||
var output = {
|
||||
props: props,
|
||||
component: this,
|
||||
controller: this.bind(undefined, props),
|
||||
view: view
|
||||
};
|
||||
if (props.key) {
|
||||
output.attrs = {key: props.key};
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
21
js/lib/components/action-button.js
Normal file
21
js/lib/components/action-button.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Component from 'flarum/component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class ActionButton extends Component {
|
||||
view() {
|
||||
var attrs = {};
|
||||
for (var i in this.props) { attrs[i] = this.props[i]; }
|
||||
|
||||
var iconName = attrs.icon;
|
||||
delete attrs.icon;
|
||||
|
||||
var label = attrs.label;
|
||||
delete attrs.label;
|
||||
|
||||
attrs.href = attrs.href || 'javascript:;';
|
||||
return m('a', attrs, [
|
||||
iconName ? icon(iconName+' icon-glyph') : '',
|
||||
m('span.label', label)
|
||||
]);
|
||||
}
|
||||
}
|
33
js/lib/components/alert.js
Normal file
33
js/lib/components/alert.js
Normal file
@ -0,0 +1,33 @@
|
||||
import Component from 'flarum/component';
|
||||
import ActionButton from 'flarum/components/action-button';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
|
||||
export default class Alert extends Component {
|
||||
view() {
|
||||
var attrs = {};
|
||||
for (var i in this.props) { attrs[i] = this.props[i]; }
|
||||
|
||||
attrs.className = (attrs.className || '') + ' alert-'+attrs.type;
|
||||
delete attrs.type;
|
||||
|
||||
var message = attrs.message;
|
||||
delete attrs.message;
|
||||
|
||||
var controlItems = attrs.controls.slice() || [];
|
||||
delete attrs.controls;
|
||||
|
||||
if (attrs.dismissible || attrs.dismissible === undefined) {
|
||||
controlItems.push(ActionButton.component({
|
||||
icon: 'times',
|
||||
className: 'btn btn-icon btn-link',
|
||||
onclick: attrs.ondismiss.bind(this)
|
||||
}));
|
||||
}
|
||||
delete attrs.dismissible;
|
||||
|
||||
return m('div.alert', attrs, [
|
||||
m('span.alert-text', message),
|
||||
m('ul.alert-controls', listItems(controlItems))
|
||||
]);
|
||||
}
|
||||
}
|
34
js/lib/components/alerts.js
Normal file
34
js/lib/components/alerts.js
Normal file
@ -0,0 +1,34 @@
|
||||
import Component from 'flarum/component';
|
||||
|
||||
export default class Alerts extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.components = [];
|
||||
}
|
||||
|
||||
view() {
|
||||
return m('div.alerts', this.components.map((component) => {
|
||||
component.props.ondismiss = this.dismiss.bind(this, component);
|
||||
return m('div.alert-wrapper', component);
|
||||
}));
|
||||
}
|
||||
|
||||
show(component) {
|
||||
this.components.push(component);
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
dismiss(component) {
|
||||
var index = this.components.indexOf(component);
|
||||
if (index !== -1) {
|
||||
this.components.splice(index, 1);
|
||||
}
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.components = [];
|
||||
m.redraw();
|
||||
}
|
||||
}
|
32
js/lib/components/back-button.js
Normal file
32
js/lib/components/back-button.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Component from 'flarum/component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
The back/pin button group in the top-left corner of Flarum's interface.
|
||||
*/
|
||||
export default class BackButton extends Component {
|
||||
view() {
|
||||
var history = app.history;
|
||||
var pane = app.pane;
|
||||
|
||||
return m('div.back-button', {
|
||||
className: this.props.className || '',
|
||||
onmouseenter: pane && pane.show.bind(pane),
|
||||
onmouseleave: pane && pane.onmouseleave.bind(pane),
|
||||
config: this.onload.bind(this)
|
||||
}, history.canGoBack() ? m('div.btn-group', [
|
||||
m('button.btn.btn-default.btn-icon.back', {onclick: history.back.bind(history)}, icon('chevron-left icon-glyph')),
|
||||
pane && pane.active ? m('button.btn.btn-default.btn-icon.pin'+(pane.active ? '.active' : ''), {onclick: pane.togglePinned.bind(pane)}, icon('thumb-tack icon-glyph')) : '',
|
||||
]) : (this.props.drawer ? [
|
||||
m('button.btn.btn-default.btn-icon.drawer-toggle', {onclick: this.toggleDrawer.bind(this)}, icon('reorder icon-glyph'))
|
||||
] : ''));
|
||||
}
|
||||
|
||||
onload(element, isInitialized, context) {
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
toggleDrawer() {
|
||||
$('body').toggleClass('drawer-open');
|
||||
}
|
||||
}
|
19
js/lib/components/badge.js
Normal file
19
js/lib/components/badge.js
Normal file
@ -0,0 +1,19 @@
|
||||
import Component from 'flarum/component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class Badge extends Component {
|
||||
view(ctrl) {
|
||||
var iconName = this.props.icon;
|
||||
var label = this.props.title = this.props.label;
|
||||
delete this.props.icon, this.props.label;
|
||||
this.props.config = function(element) {
|
||||
$(element).tooltip();
|
||||
};
|
||||
this.props.className = 'badge '+(this.props.className || '');
|
||||
|
||||
return m('span', this.props, [
|
||||
icon(iconName+' icon-glyph'),
|
||||
m('span.label', label)
|
||||
]);
|
||||
}
|
||||
}
|
20
js/lib/components/dropdown-button.js
Normal file
20
js/lib/components/dropdown-button.js
Normal file
@ -0,0 +1,20 @@
|
||||
import Component from 'flarum/component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
|
||||
export default class DropdownButton extends Component {
|
||||
view() {
|
||||
return m('div', {className: 'dropdown btn-group '+(this.props.items ? 'item-count-'+this.props.items.length : '')+' '+(this.props.className || '')}, [
|
||||
m('a[href=javascript:;]', {
|
||||
className: 'dropdown-toggle '+(this.props.buttonClass || 'btn btn-default'),
|
||||
'data-toggle': 'dropdown',
|
||||
onclick: this.props.buttonClick
|
||||
}, this.props.buttonContent || [
|
||||
icon((this.props.icon || 'ellipsis-v')+' icon-glyph'),
|
||||
m('span.label', this.props.label || 'Controls'),
|
||||
icon('caret-down icon-caret')
|
||||
]),
|
||||
m(this.props.menuContent ? 'div' : 'ul', {className: 'dropdown-menu '+(this.props.menuClass || '')}, this.props.menuContent || listItems(this.props.items))
|
||||
]);
|
||||
}
|
||||
}
|
18
js/lib/components/dropdown-select.js
Normal file
18
js/lib/components/dropdown-select.js
Normal file
@ -0,0 +1,18 @@
|
||||
import Component from 'flarum/component'
|
||||
import icon from 'flarum/helpers/icon'
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
|
||||
export default class DropdownSelect extends Component {
|
||||
view() {
|
||||
var activeItem = this.props.items.filter((item) => item.component.active && item.component.active(item.props))[0];
|
||||
var label = activeItem && activeItem.props.label;
|
||||
|
||||
return m('div', {className: 'dropdown dropdown-select btn-group item-count-'+this.props.items.length+' '+this.props.className}, [
|
||||
m('a[href=javascript:;]', {className: 'dropdown-toggle '+(this.props.buttonClass || 'btn btn-default'), 'data-toggle': 'dropdown'}, [
|
||||
m('span.label', label), ' ',
|
||||
icon('sort icon-caret')
|
||||
]),
|
||||
m('ul', {className: 'dropdown-menu '+this.props.menuClass}, listItems(this.props.items, true))
|
||||
])
|
||||
}
|
||||
}
|
30
js/lib/components/dropdown-split.js
Normal file
30
js/lib/components/dropdown-split.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Component from 'flarum/component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
import ActionButton from 'flarum/components/action-button';
|
||||
|
||||
/**
|
||||
Given a list of items, this component displays a split button: the left side
|
||||
is the first item in the list, while the right side is a dropdown-toggle
|
||||
which shows a dropdown menu containing all of the items.
|
||||
*/
|
||||
export default class DropdownSplit extends Component {
|
||||
view() {
|
||||
var firstItem = this.props.items[0];
|
||||
var items = listItems(this.props.items);
|
||||
|
||||
var buttonProps = { className: this.props.buttonClass || 'btn btn-default' };
|
||||
for (var i in firstItem.props) {
|
||||
buttonProps[i] = firstItem.props[i];
|
||||
}
|
||||
|
||||
return m('div', {className: 'dropdown dropdown-split btn-group item-count-'+(items.length)+' '+this.props.className}, [
|
||||
ActionButton.component(buttonProps),
|
||||
m('a[href=javascript:;]', {className: 'dropdown-toggle '+this.props.buttonClass, 'data-toggle': 'dropdown'}, [
|
||||
icon('caret-down icon-caret'),
|
||||
icon((this.props.icon || 'ellipsis-v')+' icon-glyph'),
|
||||
]),
|
||||
m('ul', {className: 'dropdown-menu '+(this.props.menuClass || 'pull-right')}, items)
|
||||
])
|
||||
}
|
||||
}
|
11
js/lib/components/field-set.js
Normal file
11
js/lib/components/field-set.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Component from 'flarum/component';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
|
||||
export default class FieldSet extends Component {
|
||||
view() {
|
||||
return m('fieldset', {className: this.props.className}, [
|
||||
m('legend', this.props.label),
|
||||
m('ul', listItems(this.props.fields))
|
||||
]);
|
||||
}
|
||||
}
|
15
js/lib/components/loading-indicator.js
Normal file
15
js/lib/components/loading-indicator.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Component from 'flarum/component';
|
||||
|
||||
export default class LoadingIndicator extends Component {
|
||||
view() {
|
||||
var size = this.props.size || 'small';
|
||||
delete this.props.size;
|
||||
|
||||
this.props.config = function(element) {
|
||||
$.fn.spin.presets[size].zIndex = 'auto';
|
||||
$(element).spin(size);
|
||||
};
|
||||
|
||||
return m('div.loading-indicator', this.props, m.trust(' '));
|
||||
}
|
||||
}
|
35
js/lib/components/modal.js
Normal file
35
js/lib/components/modal.js
Normal file
@ -0,0 +1,35 @@
|
||||
import Component from 'flarum/component';
|
||||
|
||||
export default class Modal extends Component {
|
||||
view() {
|
||||
return m('div.modal.fade', {config: this.onload.bind(this)}, this.component && this.component.view())
|
||||
}
|
||||
|
||||
onload(element, isInitialized) {
|
||||
if (isInitialized) { return; }
|
||||
|
||||
this.element(element);
|
||||
|
||||
this.$()
|
||||
.on('hidden.bs.modal', this.destroy.bind(this))
|
||||
.on('shown.bs.modal', this.ready.bind(this));
|
||||
}
|
||||
|
||||
show(component) {
|
||||
this.component = component;
|
||||
m.redraw(true);
|
||||
this.$().modal('show');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$().modal('hide');
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.component && this.component.ready && this.component.ready(this.$());
|
||||
}
|
||||
}
|
17
js/lib/components/nav-item.js
Normal file
17
js/lib/components/nav-item.js
Normal file
@ -0,0 +1,17 @@
|
||||
import Component from 'flarum/component'
|
||||
import icon from 'flarum/helpers/icon'
|
||||
|
||||
export default class NavItem extends Component {
|
||||
view() {
|
||||
var active = NavItem.active(this.props);
|
||||
return m('li'+(active ? '.active' : ''), m('a', {href: this.props.href, config: m.route}, [
|
||||
icon(this.props.icon),
|
||||
this.props.label, ' ',
|
||||
m('span.count', this.props.badge)
|
||||
]))
|
||||
}
|
||||
|
||||
static active(props) {
|
||||
return typeof props.active !== 'undefined' ? props.active : m.route() === props.href;
|
||||
}
|
||||
}
|
13
js/lib/components/select-input.js
Normal file
13
js/lib/components/select-input.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Component from 'flarum/component'
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class SelectInput extends Component {
|
||||
view(ctrl) {
|
||||
return m('span.select-input', [
|
||||
m('select.form-control', {onchange: m.withAttr('value', this.props.onchange.bind(ctrl)), value: this.props.value}, [
|
||||
this.props.options.map(function(option) { return m('option', {value: option.key}, option.value) })
|
||||
]),
|
||||
icon('sort')
|
||||
])
|
||||
}
|
||||
}
|
14
js/lib/components/separator.js
Normal file
14
js/lib/components/separator.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Component from 'flarum/component';
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
class Separator extends Component {
|
||||
view() {
|
||||
return m('span');
|
||||
}
|
||||
}
|
||||
|
||||
Separator.wrapperClass = 'divider';
|
||||
|
||||
export default Separator;
|
30
js/lib/components/switch-input.js
Normal file
30
js/lib/components/switch-input.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Component from 'flarum/component';
|
||||
import LoadingIndicator from 'flarum/components/loading-indicator';
|
||||
|
||||
export default class SwitchInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.loading = m.prop(false);
|
||||
}
|
||||
|
||||
view() {
|
||||
return m('div.checkbox.checkbox-switch', [
|
||||
m('label', [
|
||||
m('div.switch-control', [
|
||||
m('input[type=checkbox]', {
|
||||
checked: this.props.state,
|
||||
onchange: m.withAttr('checked', this.onchange.bind(this))
|
||||
}),
|
||||
m('div.switch', {className: this.loading() && 'loading'})
|
||||
]),
|
||||
this.props.label, ' ',
|
||||
this.loading() ? LoadingIndicator.component({size: 'tiny'}) : ''
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
onchange(checked) {
|
||||
this.props.onchange && this.props.onchange(checked, this);
|
||||
}
|
||||
}
|
65
js/lib/components/text-editor.js
Normal file
65
js/lib/components/text-editor.js
Normal file
@ -0,0 +1,65 @@
|
||||
import Component from 'flarum/component';
|
||||
import ItemList from 'flarum/utils/item-list';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
import ActionButton from 'flarum/components/action-button';
|
||||
|
||||
/**
|
||||
A text editor. Contains a textarea and an item list of `controls`, including
|
||||
a submit button.
|
||||
*/
|
||||
export default class TextEditor extends Component {
|
||||
constructor(props) {
|
||||
props.submitLabel = props.submitLabel || 'Submit';
|
||||
|
||||
super(props);
|
||||
|
||||
this.value = m.prop(this.props.value || '');
|
||||
}
|
||||
|
||||
view() {
|
||||
return m('div.text-editor', {config: this.element}, [
|
||||
m('textarea.form-control.flexible-height', {
|
||||
config: this.configTextarea.bind(this),
|
||||
onkeyup: m.withAttr('value', this.onkeyup.bind(this)),
|
||||
placeholder: this.props.placeholder || '',
|
||||
disabled: !!this.props.disabled,
|
||||
value: this.props.value || ''
|
||||
}),
|
||||
m('ul.text-editor-controls.fade', listItems(this.controlItems().toArray()))
|
||||
]);
|
||||
}
|
||||
|
||||
configTextarea(element, isInitialized) {
|
||||
if (isInitialized) { return; }
|
||||
|
||||
$(element).bind('keydown', 'meta+return', this.onsubmit.bind(this));
|
||||
}
|
||||
|
||||
controlItems() {
|
||||
var items = new ItemList();
|
||||
|
||||
items.add('submit',
|
||||
ActionButton.component({
|
||||
label: this.props.submitLabel,
|
||||
icon: 'check',
|
||||
className: 'btn btn-primary',
|
||||
wrapperClass: 'primary-control',
|
||||
onclick: this.onsubmit.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
onkeyup(value) {
|
||||
this.value(value);
|
||||
this.props.onchange(this.value());
|
||||
this.$('.text-editor-controls').toggleClass('in', !!value);
|
||||
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
this.props.onsubmit(this.value());
|
||||
}
|
||||
}
|
35
js/lib/components/yesno-input.js
Normal file
35
js/lib/components/yesno-input.js
Normal file
@ -0,0 +1,35 @@
|
||||
import Component from 'flarum/component';
|
||||
import LoadingIndicator from 'flarum/components/loading-indicator';
|
||||
import classList from 'flarum/utils/class-list';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
export default class YesNoInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.loading = m.prop(false);
|
||||
}
|
||||
|
||||
view() {
|
||||
return m('label.yesno-control', [
|
||||
m('input[type=checkbox]', {
|
||||
checked: this.props.state,
|
||||
disabled: this.props.disabled,
|
||||
onchange: m.withAttr('checked', this.onchange.bind(this))
|
||||
}),
|
||||
m('div.yesno', {className: classList({
|
||||
loading: this.loading(),
|
||||
disabled: this.props.disabled,
|
||||
state: this.props.state ? 'yes' : 'no'
|
||||
})}, [
|
||||
this.loading()
|
||||
? LoadingIndicator.component({size: 'tiny'})
|
||||
: icon(this.props.state ? 'check' : 'times')
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
onchange(checked) {
|
||||
this.props.onchange && this.props.onchange(checked, this);
|
||||
}
|
||||
}
|
8
js/lib/extension-utils.js
Normal file
8
js/lib/extension-utils.js
Normal file
@ -0,0 +1,8 @@
|
||||
export function extend(object, func, extension) {
|
||||
var oldFunc = object[func];
|
||||
object[func] = function() {
|
||||
var value = oldFunc.apply(this, arguments);
|
||||
var args = [].slice.apply(arguments);
|
||||
return extension.apply(this, [value].concat(args));
|
||||
}
|
||||
};
|
26
js/lib/helpers/avatar.js
Normal file
26
js/lib/helpers/avatar.js
Normal file
@ -0,0 +1,26 @@
|
||||
export default function avatar(user, args) {
|
||||
args = args || {}
|
||||
args.className = 'avatar '+(args.className || '')
|
||||
var content = ''
|
||||
|
||||
var title = typeof args.title === 'undefined' || args.title
|
||||
if (!title) { delete args.title }
|
||||
|
||||
if (user) {
|
||||
var username = user.username() || '?'
|
||||
|
||||
if (title) { args.title = args.title || username }
|
||||
|
||||
var avatarUrl = user.avatarUrl()
|
||||
if (avatarUrl) {
|
||||
args.src = avatarUrl
|
||||
return m('img', args)
|
||||
}
|
||||
|
||||
content = username.charAt(0).toUpperCase()
|
||||
args.style = {background: user.color()}
|
||||
}
|
||||
|
||||
if (!args.title) { delete args.title }
|
||||
return m('span', args, content)
|
||||
}
|
7
js/lib/helpers/full-time.js
Normal file
7
js/lib/helpers/full-time.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function fullTime(time) {
|
||||
var time = moment(time);
|
||||
var datetime = time.format();
|
||||
var full = time.format('LLLL');
|
||||
|
||||
return m('time', {pubdate: '', datetime}, full);
|
||||
}
|
11
js/lib/helpers/human-time.js
Normal file
11
js/lib/helpers/human-time.js
Normal file
@ -0,0 +1,11 @@
|
||||
import humanTime from 'flarum/utils/human-time';
|
||||
|
||||
export default function humanTimeHelper(time) {
|
||||
var time = moment(time);
|
||||
var datetime = time.format();
|
||||
var full = time.format('LLLL');
|
||||
|
||||
var ago = humanTime(time);
|
||||
|
||||
return m('time', {pubdate: '', datetime, title: full, 'data-humantime': ''}, ago);
|
||||
}
|
3
js/lib/helpers/icon.js
Normal file
3
js/lib/helpers/icon.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function icon(icon) {
|
||||
return m('i.fa.fa-fw.fa-'+icon)
|
||||
}
|
21
js/lib/helpers/list-items.js
Normal file
21
js/lib/helpers/list-items.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Separator from 'flarum/components/separator';
|
||||
|
||||
function isSeparator(item) {
|
||||
return item && item.component === Separator;
|
||||
}
|
||||
|
||||
export default function listItems(array, noWrap) {
|
||||
// Remove duplicate/unnecessary separators
|
||||
var prevItem;
|
||||
var newArray = [];
|
||||
array.forEach(function(item, i) {
|
||||
if ((!prevItem || isSeparator(prevItem) || i === array.length - 1) && isSeparator(item)) {
|
||||
|
||||
} else {
|
||||
prevItem = item;
|
||||
newArray.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return newArray.map(item => [(noWrap && !isSeparator(item)) ? item : m('li', {className: (item.props && item.props.wrapperClass) || (item.component && item.component.wrapperClass) || ''}, item), ' ']);
|
||||
};
|
5
js/lib/helpers/username.js
Normal file
5
js/lib/helpers/username.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default function username(user) {
|
||||
var username = (user && user.username()) || '[deleted]';
|
||||
|
||||
return m('span.username', username);
|
||||
}
|
5
js/lib/initializers/preload.js
Normal file
5
js/lib/initializers/preload.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default function(app) {
|
||||
if (app.preload.data) {
|
||||
app.store.pushPayload({data: app.preload.data});
|
||||
}
|
||||
};
|
10
js/lib/initializers/session.js
Normal file
10
js/lib/initializers/session.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Session from 'flarum/session';
|
||||
|
||||
export default function(app) {
|
||||
app.session = new Session();
|
||||
|
||||
if (app.preload.session) {
|
||||
app.session.token(app.preload.session.token);
|
||||
app.session.user(app.store.getById('users', app.preload.session.userId));
|
||||
}
|
||||
}
|
18
js/lib/initializers/store.js
Normal file
18
js/lib/initializers/store.js
Normal file
@ -0,0 +1,18 @@
|
||||
import Store from 'flarum/store';
|
||||
import User from 'flarum/models/user';
|
||||
import Discussion from 'flarum/models/discussion';
|
||||
import Post from 'flarum/models/post';
|
||||
import Group from 'flarum/models/group';
|
||||
import Activity from 'flarum/models/activity';
|
||||
import Notification from 'flarum/models/notification';
|
||||
|
||||
export default function(app) {
|
||||
app.store = new Store();
|
||||
|
||||
app.store.model('users', User);
|
||||
app.store.model('discussions', Discussion);
|
||||
app.store.model('posts', Post);
|
||||
app.store.model('groups', Group);
|
||||
app.store.model('activity', Activity);
|
||||
app.store.model('notifications', Notification);
|
||||
};
|
138
js/lib/initializers/timestamps.js
Normal file
138
js/lib/initializers/timestamps.js
Normal file
@ -0,0 +1,138 @@
|
||||
import humanTime from 'flarum/utils/human-time';
|
||||
|
||||
export default function(app) {
|
||||
// Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License
|
||||
// @todo rewrite this to be simpler and cleaner
|
||||
(function($, moment) {
|
||||
var updateInterval = 1e3,
|
||||
paused = false,
|
||||
$livestamps = $([]),
|
||||
|
||||
init = function() {
|
||||
livestampGlobal.resume();
|
||||
},
|
||||
|
||||
prep = function($el, timestamp) {
|
||||
var oldData = $el.data('livestampdata');
|
||||
if (typeof timestamp == 'number')
|
||||
timestamp *= 1e3;
|
||||
|
||||
$el.removeAttr('data-humantime')
|
||||
.removeData('humantime');
|
||||
|
||||
timestamp = moment(timestamp);
|
||||
if (moment().diff(timestamp) > 60 * 60) {
|
||||
return;
|
||||
}
|
||||
if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
|
||||
var newData = $.extend({ }, { 'original': $el.contents() }, oldData);
|
||||
newData.moment = moment(timestamp);
|
||||
|
||||
$el.data('livestampdata', newData).empty();
|
||||
$livestamps.push($el[0]);
|
||||
}
|
||||
},
|
||||
|
||||
run = function() {
|
||||
if (paused) return;
|
||||
livestampGlobal.update();
|
||||
setTimeout(run, updateInterval);
|
||||
},
|
||||
|
||||
livestampGlobal = {
|
||||
update: function() {
|
||||
$('[data-humantime]').each(function() {
|
||||
var $this = $(this);
|
||||
prep($this, $this.attr('datetime'));
|
||||
});
|
||||
|
||||
var toRemove = [];
|
||||
$livestamps.each(function() {
|
||||
var $this = $(this),
|
||||
data = $this.data('livestampdata');
|
||||
|
||||
if (data === undefined)
|
||||
toRemove.push(this);
|
||||
else if (moment.isMoment(data.moment)) {
|
||||
var from = $this.html(),
|
||||
to = humanTime(data.moment);
|
||||
// to = data.moment.fromNow();
|
||||
|
||||
if (from != to) {
|
||||
var e = $.Event('change.livestamp');
|
||||
$this.trigger(e, [from, to]);
|
||||
if (!e.isDefaultPrevented())
|
||||
$this.html(to);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$livestamps = $livestamps.not(toRemove);
|
||||
},
|
||||
|
||||
pause: function() {
|
||||
paused = true;
|
||||
},
|
||||
|
||||
resume: function() {
|
||||
paused = false;
|
||||
run();
|
||||
},
|
||||
|
||||
interval: function(interval) {
|
||||
if (interval === undefined)
|
||||
return updateInterval;
|
||||
updateInterval = interval;
|
||||
}
|
||||
},
|
||||
|
||||
livestampLocal = {
|
||||
add: function($el, timestamp) {
|
||||
if (typeof timestamp == 'number')
|
||||
timestamp *= 1e3;
|
||||
timestamp = moment(timestamp);
|
||||
|
||||
if (moment.isMoment(timestamp) && !isNaN(+timestamp)) {
|
||||
$el.each(function() {
|
||||
prep($(this), timestamp);
|
||||
});
|
||||
livestampGlobal.update();
|
||||
}
|
||||
|
||||
return $el;
|
||||
},
|
||||
|
||||
destroy: function($el) {
|
||||
$livestamps = $livestamps.not($el);
|
||||
$el.each(function() {
|
||||
var $this = $(this),
|
||||
data = $this.data('livestampdata');
|
||||
|
||||
if (data === undefined)
|
||||
return $el;
|
||||
|
||||
$this
|
||||
.html(data.original ? data.original : '')
|
||||
.removeData('livestampdata');
|
||||
});
|
||||
|
||||
return $el;
|
||||
},
|
||||
|
||||
isLivestamp: function($el) {
|
||||
return $el.data('livestampdata') !== undefined;
|
||||
}
|
||||
};
|
||||
|
||||
$.livestamp = livestampGlobal;
|
||||
$(init);
|
||||
$.fn.livestamp = function(method, options) {
|
||||
if (!livestampLocal[method]) {
|
||||
options = method;
|
||||
method = 'add';
|
||||
}
|
||||
|
||||
return livestampLocal[method](this, options);
|
||||
};
|
||||
})(jQuery, moment);
|
||||
}
|
88
js/lib/model.js
Normal file
88
js/lib/model.js
Normal file
@ -0,0 +1,88 @@
|
||||
export default class Model {
|
||||
constructor(data, store) {
|
||||
this.data = m.prop(data || {});
|
||||
this.freshness = new Date();
|
||||
this.exists = false;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
pushData(newData) {
|
||||
var data = this.data();
|
||||
|
||||
for (var i in newData) {
|
||||
if (i === 'links') {
|
||||
data[i] = data[i] || {};
|
||||
for (var j in newData[i]) {
|
||||
if (newData[i][j] instanceof Model) {
|
||||
newData[i][j] = {linkage: {type: newData[i][j].data().type, id: newData[i][j].data().id}};
|
||||
}
|
||||
data[i][j] = newData[i][j];
|
||||
}
|
||||
} else {
|
||||
data[i] = newData[i];
|
||||
}
|
||||
}
|
||||
|
||||
this.freshness = new Date();
|
||||
}
|
||||
|
||||
save(data) {
|
||||
if (data.links) {
|
||||
for (var i in data.links) {
|
||||
var model = data.links[i];
|
||||
data.links[i] = {linkage: {type: model.data().type, id: model.data().id}};
|
||||
}
|
||||
}
|
||||
|
||||
this.pushData(data);
|
||||
|
||||
return m.request({
|
||||
method: this.exists ? 'PUT' : 'POST',
|
||||
url: app.config.apiURL+'/'+this.data().type+(this.exists ? '/'+this.data().id : ''),
|
||||
data: {data},
|
||||
background: true,
|
||||
config: app.session.authorize.bind(app.session)
|
||||
}).then(payload => {
|
||||
this.store.data[payload.data.type][payload.data.id] = this;
|
||||
return this.store.pushPayload(payload);
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
if (!this.exists) { return; }
|
||||
|
||||
return m.request({
|
||||
method: 'DELETE',
|
||||
url: app.config.apiURL+'/'+this.data().type+'/'+this.data().id,
|
||||
background: true,
|
||||
config: app.session.authorize.bind(app.session)
|
||||
}).then(() => this.exists = false);
|
||||
}
|
||||
|
||||
static prop(name, transform) {
|
||||
return function() {
|
||||
var data = this.data()[name];
|
||||
return transform ? transform(data) : data
|
||||
}
|
||||
}
|
||||
|
||||
static one(name) {
|
||||
return function() {
|
||||
var link = this.data().links[name];
|
||||
return link && app.store.getById(link.linkage.type, link.linkage.id)
|
||||
}
|
||||
}
|
||||
|
||||
static many(name) {
|
||||
return function() {
|
||||
var link = this.data().links[name];
|
||||
return link && link.linkage.map(function(link) {
|
||||
return app.store.getById(link.type, link.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static date(data) {
|
||||
return data ? new Date(data) : null;
|
||||
}
|
||||
}
|
14
js/lib/models/activity.js
Normal file
14
js/lib/models/activity.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Model from 'flarum/model';
|
||||
|
||||
class Activity extends Model {}
|
||||
|
||||
Activity.prototype.id = Model.prop('id');
|
||||
Activity.prototype.contentType = Model.prop('contentType');
|
||||
Activity.prototype.content = Model.prop('content');
|
||||
Activity.prototype.time = Model.prop('time', Model.date);
|
||||
|
||||
Activity.prototype.user = Model.one('user');
|
||||
Activity.prototype.sender = Model.one('sender');
|
||||
Activity.prototype.post = Model.one('post');
|
||||
|
||||
export default Activity;
|
45
js/lib/models/discussion.js
Normal file
45
js/lib/models/discussion.js
Normal file
@ -0,0 +1,45 @@
|
||||
import Model from 'flarum/model';
|
||||
import computed from 'flarum/utils/computed';
|
||||
import ItemList from 'flarum/utils/item-list';
|
||||
|
||||
class Discussion extends Model {}
|
||||
|
||||
Discussion.prototype.id = Model.prop('id');
|
||||
Discussion.prototype.title = Model.prop('title');
|
||||
Discussion.prototype.slug = computed('title', title => title.toLowerCase().replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/-$|^-/g, ''));
|
||||
|
||||
Discussion.prototype.startTime = Model.prop('startTime', Model.date);
|
||||
Discussion.prototype.startUser = Model.one('startUser');
|
||||
Discussion.prototype.startPost = Model.one('startPost');
|
||||
|
||||
Discussion.prototype.lastTime = Model.prop('lastTime', Model.date);
|
||||
Discussion.prototype.lastUser = Model.one('lastUser');
|
||||
Discussion.prototype.lastPost = Model.one('lastPost');
|
||||
Discussion.prototype.lastPostNumber = Model.prop('lastPostNumber');
|
||||
|
||||
Discussion.prototype.canReply = Model.prop('canReply');
|
||||
Discussion.prototype.canEdit = Model.prop('canEdit');
|
||||
Discussion.prototype.canDelete = Model.prop('canDelete');
|
||||
|
||||
Discussion.prototype.commentsCount = Model.prop('commentsCount');
|
||||
Discussion.prototype.repliesCount = computed('commentsCount', commentsCount => commentsCount - 1);
|
||||
|
||||
Discussion.prototype.posts = Model.many('posts');
|
||||
Discussion.prototype.relevantPosts = Model.many('relevantPosts');
|
||||
Discussion.prototype.addedPosts = Model.many('addedPosts');
|
||||
|
||||
Discussion.prototype.readTime = Model.prop('readTime', Model.date);
|
||||
Discussion.prototype.readNumber = Model.prop('readNumber');
|
||||
|
||||
Discussion.prototype.unreadCount = function() {
|
||||
var user = app.session.user();
|
||||
if (user && user.readTime() < this.lastTime()) {
|
||||
return Math.max(0, this.lastPostNumber() - (this.readNumber() || 0))
|
||||
}
|
||||
return 0
|
||||
};
|
||||
Discussion.prototype.isUnread = computed('unreadCount', unreadCount => !!unreadCount);
|
||||
|
||||
Discussion.prototype.badges = () => new ItemList();
|
||||
|
||||
export default Discussion;
|
8
js/lib/models/group.js
Normal file
8
js/lib/models/group.js
Normal file
@ -0,0 +1,8 @@
|
||||
import Model from 'flarum/model';
|
||||
|
||||
class Group extends Model {}
|
||||
|
||||
Group.prototype.id = Model.prop('id');
|
||||
Group.prototype.name = Model.prop('name');
|
||||
|
||||
export default Group;
|
19
js/lib/models/notification.js
Normal file
19
js/lib/models/notification.js
Normal file
@ -0,0 +1,19 @@
|
||||
import Model from 'flarum/model';
|
||||
import computed from 'flarum/utils/computed';
|
||||
|
||||
class Notification extends Model {}
|
||||
|
||||
Notification.prototype.id = Model.prop('id');
|
||||
Notification.prototype.contentType = Model.prop('contentType');
|
||||
Notification.prototype.subjectId = Model.prop('subjectId');
|
||||
Notification.prototype.content = Model.prop('content');
|
||||
Notification.prototype.time = Model.prop('time', Model.date);
|
||||
Notification.prototype.isRead = Model.prop('isRead');
|
||||
Notification.prototype.unreadCount = Model.prop('unreadCount');
|
||||
Notification.prototype.additionalUnreadCount = computed('unreadCount', unreadCount => Math.max(0, unreadCount - 1));
|
||||
|
||||
Notification.prototype.user = Model.one('user');
|
||||
Notification.prototype.sender = Model.one('sender');
|
||||
Notification.prototype.subject = Model.one('subject');
|
||||
|
||||
export default Notification;
|
27
js/lib/models/post.js
Normal file
27
js/lib/models/post.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Model from 'flarum/model';
|
||||
import computed from 'flarum/utils/computed';
|
||||
|
||||
class Post extends Model {}
|
||||
|
||||
Post.prototype.id = Model.prop('id');
|
||||
Post.prototype.number = Model.prop('number');
|
||||
Post.prototype.discussion = Model.one('discussion');
|
||||
|
||||
Post.prototype.time = Model.prop('time');
|
||||
Post.prototype.user = Model.one('user');
|
||||
Post.prototype.contentType = Model.prop('contentType');
|
||||
Post.prototype.content = Model.prop('content');
|
||||
Post.prototype.contentHtml = Model.prop('contentHtml');
|
||||
|
||||
Post.prototype.editTime = Model.prop('editTime', Model.date);
|
||||
Post.prototype.editUser = Model.one('editUser');
|
||||
Post.prototype.isEdited = computed('editTime', editTime => !!editTime);
|
||||
|
||||
Post.prototype.hideTime = Model.prop('hideTime', Model.date);
|
||||
Post.prototype.hideUser = Model.one('hideUser');
|
||||
Post.prototype.isHidden = computed('hideTime', hideTime => !!hideTime);
|
||||
|
||||
Post.prototype.canEdit = Model.prop('canEdit');
|
||||
Post.prototype.canDelete = Model.prop('canDelete');
|
||||
|
||||
export default Post;
|
53
js/lib/models/user.js
Normal file
53
js/lib/models/user.js
Normal file
@ -0,0 +1,53 @@
|
||||
import Model from 'flarum/model'
|
||||
import stringToColor from 'flarum/utils/string-to-color';
|
||||
import ItemList from 'flarum/utils/item-list';
|
||||
import computed from 'flarum/utils/computed';
|
||||
|
||||
class User extends Model {}
|
||||
|
||||
User.prototype.id = Model.prop('id');
|
||||
User.prototype.username = Model.prop('username');
|
||||
User.prototype.email = Model.prop('email');
|
||||
User.prototype.isConfirmed = Model.prop('isConfirmed');
|
||||
User.prototype.password = Model.prop('password');
|
||||
User.prototype.avatarUrl = Model.prop('avatarUrl');
|
||||
User.prototype.bio = Model.prop('bio');
|
||||
User.prototype.bioHtml = Model.prop('bioHtml');
|
||||
User.prototype.preferences = Model.prop('preferences');
|
||||
|
||||
User.prototype.groups = Model.many('groups');
|
||||
|
||||
User.prototype.joinTime = Model.prop('joinTime', Model.date);
|
||||
User.prototype.lastSeenTime = Model.prop('lastSeenTime', Model.date);
|
||||
User.prototype.online = function() { return this.lastSeenTime() > moment().subtract(5, 'minutes').toDate(); };
|
||||
User.prototype.readTime = Model.prop('readTime', Model.date);
|
||||
User.prototype.unreadNotificationsCount = Model.prop('unreadNotificationsCount');
|
||||
|
||||
User.prototype.discussionsCount = Model.prop('discussionsCount');
|
||||
User.prototype.commentsCount = Model.prop('commentsCount');
|
||||
;
|
||||
User.prototype.canEdit = Model.prop('canEdit');
|
||||
User.prototype.canDelete = Model.prop('canDelete');
|
||||
|
||||
User.prototype.color = computed('username', 'avatarUrl', 'avatarColor', function(username, avatarUrl, avatarColor) {
|
||||
if (avatarColor) {
|
||||
return 'rgb('+avatarColor[0]+', '+avatarColor[1]+', '+avatarColor[2]+')';
|
||||
} else if (avatarUrl) {
|
||||
var image = new Image();
|
||||
var user = this;
|
||||
image.onload = function() {
|
||||
var colorThief = new ColorThief();
|
||||
user.avatarColor = colorThief.getColor(this);
|
||||
user.freshness = new Date();
|
||||
m.redraw();
|
||||
};
|
||||
image.src = avatarUrl;
|
||||
return '';
|
||||
} else {
|
||||
return '#'+stringToColor(username);
|
||||
}
|
||||
});
|
||||
|
||||
User.prototype.badges = () => new ItemList();
|
||||
|
||||
export default User;
|
41
js/lib/session.js
Normal file
41
js/lib/session.js
Normal file
@ -0,0 +1,41 @@
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import evented from 'flarum/utils/evented';
|
||||
|
||||
export default class Session extends mixin(class {}, evented) {
|
||||
constructor() {
|
||||
super();
|
||||
this.user = m.prop();
|
||||
this.token = m.prop();
|
||||
}
|
||||
|
||||
login(identification, password) {
|
||||
var deferred = m.deferred();
|
||||
var self = this;
|
||||
m.request({
|
||||
method: 'POST',
|
||||
url: app.config.baseURL+'/login',
|
||||
data: {identification, password},
|
||||
background: true
|
||||
}).then(function(response) {
|
||||
self.token(response.token);
|
||||
m.startComputation();
|
||||
app.store.find('users', response.userId).then(function(user) {
|
||||
self.user(user);
|
||||
deferred.resolve(user);
|
||||
self.trigger('loggedIn', user);
|
||||
m.endComputation();
|
||||
});
|
||||
}, function(response) {
|
||||
deferred.reject(response);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
logout() {
|
||||
window.location = app.config.baseURL+'/logout';
|
||||
}
|
||||
|
||||
authorize(xhr) {
|
||||
xhr.setRequestHeader('Authorization', 'Token '+this.token());
|
||||
}
|
||||
}
|
67
js/lib/store.js
Normal file
67
js/lib/store.js
Normal file
@ -0,0 +1,67 @@
|
||||
export default class Store {
|
||||
constructor() {
|
||||
this.data = {}
|
||||
this.models = {}
|
||||
}
|
||||
|
||||
pushPayload(payload) {
|
||||
payload.included && payload.included.map(this.pushObject.bind(this))
|
||||
var result = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);
|
||||
result.meta = payload.meta;
|
||||
result.payload = payload;
|
||||
return result;
|
||||
}
|
||||
|
||||
pushObject(data) {
|
||||
if (!this.models[data.type]) { return; }
|
||||
var type = this.data[data.type] = this.data[data.type] || {};
|
||||
|
||||
if (type[data.id]) {
|
||||
type[data.id].pushData(data);
|
||||
} else {
|
||||
type[data.id] = this.createRecord(data.type, data);
|
||||
}
|
||||
type[data.id].exists = true;
|
||||
return type[data.id];
|
||||
}
|
||||
|
||||
find(type, id, query) {
|
||||
var endpoint = type
|
||||
var params = {}
|
||||
if (id instanceof Array) {
|
||||
endpoint += '?ids[]='+id.join('&ids[]=');
|
||||
params = query
|
||||
} else if (typeof id === 'object') {
|
||||
params = id
|
||||
} else if (id) {
|
||||
endpoint += '/'+id
|
||||
params = query
|
||||
}
|
||||
return m.request({
|
||||
method: 'GET',
|
||||
url: app.config.apiURL+'/'+endpoint,
|
||||
data: params,
|
||||
background: true,
|
||||
config: app.session.authorize.bind(app.session)
|
||||
}).then(this.pushPayload.bind(this));
|
||||
}
|
||||
|
||||
getById(type, id) {
|
||||
return this.data[type] && this.data[type][id];
|
||||
}
|
||||
|
||||
all(type) {
|
||||
return this.data[type] || {};
|
||||
}
|
||||
|
||||
model(type, Model) {
|
||||
this.models[type] = Model;
|
||||
}
|
||||
|
||||
createRecord(type, data) {
|
||||
data = data || {};
|
||||
data.type = data.type || type;
|
||||
|
||||
return new (this.models[type])(data, this);
|
||||
}
|
||||
}
|
3
js/lib/utils/abbreviate-number.js
Normal file
3
js/lib/utils/abbreviate-number.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default function(number) {
|
||||
return ''+number; // todo
|
||||
}
|
21
js/lib/utils/app.js
Normal file
21
js/lib/utils/app.js
Normal file
@ -0,0 +1,21 @@
|
||||
import ItemList from 'flarum/utils/item-list';
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.initializers = new ItemList();
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
boot() {
|
||||
this.initializers.toArray().forEach((initializer) => initializer(this));
|
||||
}
|
||||
|
||||
route(name, args, queryParams) {
|
||||
var queryString = m.route.buildQueryString(queryParams);
|
||||
return this.routes[name][0].replace(/:([^\/]+)/g, function(m, t) {
|
||||
return typeof args[t] === 'function' ? args[t]() : args[t];
|
||||
}) + (queryString ? '?'+queryString : '');
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
12
js/lib/utils/class-list.js
Normal file
12
js/lib/utils/class-list.js
Normal file
@ -0,0 +1,12 @@
|
||||
export default function classList(classes) {
|
||||
var classNames = [];
|
||||
for (var i in classes) {
|
||||
var value = classes[i];
|
||||
if (value === true) {
|
||||
classNames.push(i);
|
||||
} else if (value) {
|
||||
classNames.push(value);
|
||||
}
|
||||
}
|
||||
return classNames.join(' ');
|
||||
}
|
22
js/lib/utils/computed.js
Normal file
22
js/lib/utils/computed.js
Normal file
@ -0,0 +1,22 @@
|
||||
export default function computed() {
|
||||
var args = [].slice.apply(arguments);
|
||||
var keys = args.slice(0, -1);
|
||||
var compute = args.slice(-1)[0];
|
||||
|
||||
var values = {};
|
||||
var computed;
|
||||
return function() {
|
||||
var recompute = false;
|
||||
keys.forEach(function(key) {
|
||||
var value = typeof this[key] === 'function' ? this[key]() : this[key];
|
||||
if (values[key] !== value) {
|
||||
recompute = true;
|
||||
values[key] = value;
|
||||
}
|
||||
}.bind(this));
|
||||
if (recompute) {
|
||||
computed = compute.apply(this, keys.map((key) => values[key]));
|
||||
}
|
||||
return computed;
|
||||
}
|
||||
};
|
36
js/lib/utils/evented.js
Normal file
36
js/lib/utils/evented.js
Normal file
@ -0,0 +1,36 @@
|
||||
export default {
|
||||
handlers: null,
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
getHandlers(event) {
|
||||
this.handlers = this.handlers || {};
|
||||
return this.handlers[event] = this.handlers[event] || [];
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
trigger(event, ...args) {
|
||||
this.getHandlers(event).forEach((handler) => handler.apply(this, args));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
on(event, handler) {
|
||||
this.getHandlers(event).push(handler);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
*/
|
||||
off(event, handler) {
|
||||
var handlers = this.getHandlers(event);
|
||||
var index = handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
40
js/lib/utils/human-time.js
Normal file
40
js/lib/utils/human-time.js
Normal file
@ -0,0 +1,40 @@
|
||||
moment.locale('en', {
|
||||
relativeTime : {
|
||||
future: "in %s",
|
||||
past: "%s ago",
|
||||
s: "seconds",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
M: "a month",
|
||||
MM: "%d months",
|
||||
y: "a year",
|
||||
yy: "%d years"
|
||||
}
|
||||
});
|
||||
|
||||
export default function humanTime(time) {
|
||||
var m = moment(time);
|
||||
|
||||
var minute = 6e4;
|
||||
var hour = 36e5;
|
||||
var day = 864e5;
|
||||
var ago = null;
|
||||
|
||||
var diff = m.diff(moment());
|
||||
|
||||
if (diff < -30 * day) {
|
||||
if (m.year() === moment().year()) {
|
||||
ago = m.format('D MMM');
|
||||
} else {
|
||||
ago = m.format('MMM \'YY');
|
||||
}
|
||||
} else {
|
||||
ago = m.fromNow();
|
||||
}
|
||||
|
||||
return ago;
|
||||
};
|
55
js/lib/utils/item-list.js
Normal file
55
js/lib/utils/item-list.js
Normal file
@ -0,0 +1,55 @@
|
||||
export class Item {
|
||||
constructor(content, position) {
|
||||
this.content = content;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ItemList {
|
||||
add(key, content, position) {
|
||||
this[key] = new Item(content, position);
|
||||
}
|
||||
|
||||
toArray() {
|
||||
var items = [];
|
||||
for (var i in this) {
|
||||
if (this.hasOwnProperty(i) && this[i] instanceof Item) {
|
||||
items.push(this[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var array = [];
|
||||
|
||||
var addItems = function(method, position) {
|
||||
items = items.filter(function(item) {
|
||||
if ((position && item.position && item.position[position]) || (!position && !item.position)) {
|
||||
array[method](item);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
addItems('unshift', 'first');
|
||||
addItems('push', false);
|
||||
addItems('push', 'last');
|
||||
|
||||
items = items.filter(function(item) {
|
||||
var key = item.position.before || item.position.after;
|
||||
var type = item.position.before ? 'before' : 'after';
|
||||
if (key) {
|
||||
var index = array.indexOf(this[key]);
|
||||
if (index === -1) {
|
||||
console.log("Can't find item with key '"+key+"' to insert "+type+", inserting at end instead");
|
||||
return true;
|
||||
} else {
|
||||
array.splice(array.indexOf(this[key]) + (type === 'after' ? 1 : 0), 0, item);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
array = array.concat(items);
|
||||
|
||||
return array.map((item) => item.content);
|
||||
}
|
||||
}
|
||||
|
7
js/lib/utils/map-routes.js
Normal file
7
js/lib/utils/map-routes.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default function mapRoutes(routes) {
|
||||
var map = {};
|
||||
for (var r in routes) {
|
||||
map[routes[r][0]] = routes[r][1];
|
||||
}
|
||||
return map;
|
||||
}
|
11
js/lib/utils/mixin.js
Normal file
11
js/lib/utils/mixin.js
Normal file
@ -0,0 +1,11 @@
|
||||
export default function mixin(Parent, ...mixins) {
|
||||
class Mixed extends Parent {}
|
||||
for (var i in mixins) {
|
||||
var keys = Object.keys(mixins[i]);
|
||||
for (var j in keys) {
|
||||
var prop = keys[j];
|
||||
Mixed.prototype[prop] = mixins[i][prop];
|
||||
}
|
||||
}
|
||||
return Mixed;
|
||||
}
|
43
js/lib/utils/scroll-listener.js
Normal file
43
js/lib/utils/scroll-listener.js
Normal file
@ -0,0 +1,43 @@
|
||||
var scroll = window.requestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
window.mozRequestAnimationFrame ||
|
||||
window.msRequestAnimationFrame ||
|
||||
window.oRequestAnimationFrame ||
|
||||
function(callback) { window.setTimeout(callback, 1000/60) };
|
||||
|
||||
export default class ScrollListener {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
this.lastTop = -1;
|
||||
}
|
||||
|
||||
loop() {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.update();
|
||||
|
||||
scroll(this.loop.bind(this));
|
||||
}
|
||||
|
||||
update(force) {
|
||||
var top = window.pageYOffset;
|
||||
|
||||
if (this.lastTop !== top || force) {
|
||||
this.callback(top);
|
||||
this.lastTop = top;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.active) {
|
||||
this.active = true;
|
||||
this.loop();
|
||||
}
|
||||
}
|
||||
}
|
34
js/lib/utils/string-to-color.js
Normal file
34
js/lib/utils/string-to-color.js
Normal file
@ -0,0 +1,34 @@
|
||||
function hsvToRgb(h, s, v) {
|
||||
var r, g, b, i, f, p, q, t;
|
||||
if (h && s === undefined && v === undefined) {
|
||||
s = h.s; v = h.v; h = h.h;
|
||||
}
|
||||
i = Math.floor(h * 6);
|
||||
f = h * 6 - i;
|
||||
p = v * (1 - s);
|
||||
q = v * (1 - f * s);
|
||||
t = v * (1 - (1 - f) * s);
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q; break;
|
||||
}
|
||||
return {
|
||||
r: Math.floor(r * 255),
|
||||
g: Math.floor(g * 255),
|
||||
b: Math.floor(b * 255)
|
||||
};
|
||||
}
|
||||
|
||||
export default function stringToColor(string) {
|
||||
var num = 0;
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
num += string.charCodeAt(i);
|
||||
}
|
||||
var hue = num % 360;
|
||||
var rgb = hsvToRgb(hue / 360, 0.4, 0.9);
|
||||
return ''+rgb.r.toString(16)+rgb.g.toString(16)+rgb.b.toString(16);
|
||||
};
|
33
js/lib/utils/subtree-retainer.js
Normal file
33
js/lib/utils/subtree-retainer.js
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
// constructor
|
||||
this.subtree = new SubtreeRetainer(
|
||||
() => this.props.post.freshness,
|
||||
() => this.showing
|
||||
);
|
||||
this.subtree.add(() => this.props.user.freshness);
|
||||
|
||||
// view
|
||||
this.subtree.retain() || 'expensive expression'
|
||||
*/
|
||||
export default class SubtreeRetainer {
|
||||
constructor() {
|
||||
this.old = [];
|
||||
this.callbacks = [].slice.call(arguments);
|
||||
}
|
||||
|
||||
retain() {
|
||||
var needsRebuild = false;
|
||||
this.callbacks.forEach((callback, i) => {
|
||||
var result = callback();
|
||||
if (result !== this.old[i]) {
|
||||
this.old[i] = result;
|
||||
needsRebuild = true;
|
||||
}
|
||||
});
|
||||
return needsRebuild ? false : {subtree: 'retain'};
|
||||
}
|
||||
|
||||
add() {
|
||||
this.callbacks = this.callbacks.concat([].slice.call(arguments));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user