add prettier config and prettify javascript

This commit is contained in:
David Sevilla Martin
2020-01-31 17:17:46 -05:00
parent dd13ff4169
commit dfedd585f5
74 changed files with 3132 additions and 3007 deletions

File diff suppressed because one or more lines are too long

2
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production",
"lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write \"src/**/*\""
"lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write \"src/**/*\" \"*.{ts,js}\""
},
"devDependencies": {
"@types/classnames": "^2.2.9",

View File

@ -56,7 +56,7 @@ export default abstract class Application {
discussions: Discussion,
posts: Post,
groups: Group,
notifications: Notification
notifications: Notification,
});
drawer = new Drawer();
@ -97,10 +97,7 @@ export default abstract class Application {
this.forum = this.store.getById('forums', 1);
this.session = new Session(
this.store.getById('users', this.data.session.userId),
this.data.session.csrfToken
);
this.session = new Session(this.store.getById('users', this.data.session.userId), this.data.session.csrfToken);
this.locale();
this.plugins();
@ -156,9 +153,7 @@ export default abstract class Application {
}
updateTitle() {
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') +
(this.title ? this.title + ' - ' : '') +
this.forum.attribute('title');
document.title = (this.titleCount ? `(${this.titleCount}) ` : '') + (this.title ? this.title + ' - ' : '') + this.forum.attribute('title');
}
/**
@ -206,7 +201,9 @@ export default abstract class Application {
// error message to the user instead.
options.deserialize = options.deserialize || (responseText => responseText);
options.errorHandler = options.errorHandler || (error => {
options.errorHandler =
options.errorHandler ||
(error => {
throw error;
});
@ -250,8 +247,9 @@ export default abstract class Application {
// return new Promise((resolve, reject) => )
return m.request(options)
.then(res => res, error => {
return m.request(options).then(
res => res,
error => {
this.requestError = error;
let children;
@ -292,8 +290,8 @@ export default abstract class Application {
className: 'Button Button--link',
onclick: this.showDebug.bind(this, error),
children: 'DEBUG', // TODO make translatable
})
]
}),
],
});
try {
@ -304,7 +302,8 @@ export default abstract class Application {
}
return Promise.reject(error);
});
}
);
}
private showDebug(error: RequestError) {

View File

@ -1,12 +1,12 @@
import Mithril from 'mithril';
export type ComponentProps = {
children?: Mithril.Children,
children?: Mithril.Children;
className?: string;
[key: string]: any;
}
};
export default class Component<T extends ComponentProps = any> {
element: HTMLElement;

View File

@ -4,7 +4,7 @@
*
* @abstract
*/
import Store from "./Store";
import Store from './Store';
export default class Model {
/**
@ -112,7 +112,7 @@ export default class Model {
type: this.data.type,
id: this.data.id,
attributes,
relationships: undefined
relationships: undefined,
};
// If a 'relationships' key exists, extract it from the attributes hash and
@ -125,9 +125,7 @@ export default class Model {
const model = attributes.relationships[key];
data.relationships[key] = {
data: model instanceof Array
? model.map(Model.getIdentifier)
: Model.getIdentifier(model)
data: model instanceof Array ? model.map(Model.getIdentifier) : Model.getIdentifier(model),
};
}
@ -144,11 +142,18 @@ export default class Model {
const request = { data };
if (options.meta) request.meta = options.meta;
return app.request(Object.assign({
return app
.request(
Object.assign(
{
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
body: request
}, options)).then(
body: request,
},
options
)
)
.then(
// If everything went well, we'll make sure the store knows that this
// model exists now (if it didn't already), and we'll push the data that
// the API returned into the store.
@ -179,11 +184,18 @@ export default class Model {
delete(body = {}, options = {}) {
if (!this.exists) return Promise.resolve();
return app.request(Object.assign({
return app
.request(
Object.assign(
{
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
body
}, options)).then(() => {
body,
},
options
)
)
.then(() => {
this.exists = false;
this.store.remove(this);
});
@ -271,10 +283,10 @@ export default class Model {
/**
* Get a resource identifier object for the given model.
*/
protected static getIdentifier(model: Model): { type: string, id: string } {
protected static getIdentifier(model: Model): { type: string; id: string } {
return {
type: model.data.type,
id: model.data.id
id: model.data.id,
};
}
}

View File

@ -24,12 +24,17 @@ export default class Session {
/**
* Attempt to log in a user.
*/
login(body: { identification: string, password: string, remember?: string }, options = {}) {
return app.request(Object.assign({
login(body: { identification: string; password: string; remember?: string }, options = {}) {
return app.request(
Object.assign(
{
method: 'POST',
url: `${app.forum.attribute('baseUrl')}/login`,
body
}, options));
body,
},
options
)
);
}
/**

View File

@ -28,12 +28,10 @@ export default class Store {
* @return The model(s) representing the resource(s) contained
* within the 'data' key of the payload.
*/
pushPayload(payload: { included?: {}[], data?: {}|{}[] }): Model|Model[] {
pushPayload(payload: { included?: {}[]; data?: {} | {}[] }): Model | Model[] {
if (payload.included) payload.included.map(this.pushObject.bind(this));
const result: any = payload.data instanceof Array
? payload.data.map(this.pushObject.bind(this))
: this.pushObject(payload.data);
const result: any = payload.data instanceof Array ? payload.data.map(this.pushObject.bind(this)) : this.pushObject(payload.data);
// Attach the original payload to the model that we give back. This is
// useful to consumers as it allows them to access meta information
@ -54,7 +52,7 @@ export default class Store {
pushObject(data): Model {
if (!this.models[data.type]) return null;
const type = this.data[data.type] = this.data[data.type] || {};
const type = (this.data[data.type] = this.data[data.type] || {});
if (type[data.id]) {
type[data.id].pushData(data);
@ -89,11 +87,18 @@ export default class Store {
url += `/${id}`;
}
return app.request(Object.assign({
return app
.request(
Object.assign(
{
method: 'GET',
url,
params
}, options)).then(this.pushPayload.bind(this));
params,
},
options
)
)
.then(this.pushPayload.bind(this));
}
/**
@ -103,7 +108,7 @@ export default class Store {
* @param id The resource ID.
*/
getById<T extends Model = Model>(type: string, id: number): T {
return this.data[type] && this.data[type][id] as T;
return this.data[type] && (this.data[type][id] as T);
}
/**
@ -142,6 +147,6 @@ export default class Store {
createRecord<T extends Model = Model>(type: string, data: any = {}): T {
data.type = data.type || type;
return new (this.models[type])(data, this);
return new this.models[type](data, this);
}
}

View File

@ -5,5 +5,5 @@ import Modal from './components/Modal';
export default {
extend: extend,
'components/Modal': Modal
'components/Modal': Modal,
};

View File

@ -2,14 +2,14 @@ import Component, {ComponentProps} from '../Component';
import Button from './Button';
import listItems from '../helpers/listItems';
import extract from '../utils/extract';
import * as Mithril from "mithril";
import * as Mithril from 'mithril';
export interface AlertProps extends ComponentProps {
controls?: Mithril.ChildArray,
type?: string,
dismissible?: boolean,
controls?: Mithril.ChildArray;
type?: string;
dismissible?: boolean;
ondismiss?: () => any,
ondismiss?: () => any;
}
/**
@ -44,22 +44,13 @@ export default class Alert extends Component<AlertProps> {
const dismissControl = [];
if (dismissible || dismissible === undefined) {
dismissControl.push(
<Button
icon="fas fa-times"
className="Button Button--link Button--icon Alert-dismiss"
onclick={ondismiss}/>
);
dismissControl.push(<Button icon="fas fa-times" className="Button Button--link Button--icon Alert-dismiss" onclick={ondismiss} />);
}
return (
<div {...attrs}>
<span className="Alert-body">
{children}
</span>
<ul className="Alert-controls">
{listItems(controls.concat(dismissControl))}
</ul>
<span className="Alert-body">{children}</span>
<ul className="Alert-controls">{listItems(controls.concat(dismissControl))}</ul>
</div>
);
}

View File

@ -24,11 +24,7 @@ export default class Badge extends Component {
attrs.className = `Badge ${type ? `Badge--${type}` : ''} ${attrs.className || ''}`;
attrs.title = extract(attrs, 'label') || '';
return (
<span {...attrs}>
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust('&nbsp;')}
</span>
);
return <span {...attrs}>{iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust('&nbsp;')}</span>;
}
oncreate(vnode) {

View File

@ -5,13 +5,13 @@ import extractText from '../utils/extractText';
import LoadingIndicator from './LoadingIndicator';
export interface ButtonProps extends ComponentProps {
title?: string,
type?: string,
icon?: string,
title?: string;
type?: string;
icon?: string;
loading?: boolean,
disabled?: boolean,
onclick?: Function
loading?: boolean;
disabled?: boolean;
onclick?: Function;
}
/**
@ -67,7 +67,7 @@ export default class Button<T extends ButtonProps = ButtonProps> extends Compone
return [
iconName && iconName !== true ? icon(iconName, { className: 'Button-icon' }) : '',
children ? <span className="Button-label">{children}</span> : '',
loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
loading ? LoadingIndicator.component({ size: 'tiny', className: 'LoadingIndicator--inline' }) : '',
];
}
}

View File

@ -74,19 +74,13 @@ export default class Dropdown<T extends DropdownProps = DropdownProps> extends C
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$menu.toggleClass(
'Dropdown-menu--top',
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
);
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass(
'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
);
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
});
this.element.addEventListener('hidden.bs.dropdown', () => {
@ -105,10 +99,7 @@ export default class Dropdown<T extends DropdownProps = DropdownProps> extends C
*/
protected getButton(): any {
return (
<button
className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown"
onclick={this.props.onclick}>
<button className={'Dropdown-toggle ' + this.props.buttonClassName} data-toggle="dropdown" onclick={this.props.onclick}>
{this.getButtonContent()}
</button>
);
@ -125,15 +116,11 @@ export default class Dropdown<T extends DropdownProps = DropdownProps> extends C
return [
attrs.icon ? icon(attrs.icon, { className: 'Button-icon' }) : '',
<span className="Button-label">{attrs.label}</span>,
attrs.caretIcon ? icon(attrs.caretIcon, {className: 'Button-caret'}) : ''
attrs.caretIcon ? icon(attrs.caretIcon, { className: 'Button-caret' }) : '',
];
}
protected getMenu(items) {
return (
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
{items}
</ul>
);
return <ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>{items}</ul>;
}
}

View File

@ -36,8 +36,6 @@ export default class LinkButton extends Button<LinkButtonProps> {
* Determine whether a component with the given props is 'active'.
*/
static isActive(props: LinkButtonProps): boolean {
return typeof props.active !== 'undefined'
? props.active
: m.route.get() === props.href;
return typeof props.active !== 'undefined' ? props.active : m.route.get() === props.href;
}
}

View File

@ -2,7 +2,7 @@ import Mithril from 'mithril';
import Component, { ComponentProps } from '../Component';
import Button from './Button';
import RequestError from "../utils/RequestError";
import RequestError from '../utils/RequestError';
/**
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
@ -29,10 +29,12 @@ export default abstract class Modal<T extends ComponentProps = ComponentProps> e
{Button.component({
icon: 'fas fa-times',
onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link'
className: 'Button Button--icon Button--link',
})}
</div>
) : ''}
) : (
''
)}
<form onsubmit={this.onsubmit.bind(this)}>
<div className="Modal-header">
@ -85,7 +87,11 @@ export default abstract class Modal<T extends ComponentProps = ComponentProps> e
* Focus on the first input when the modal is ready to be used.
*/
onready() {
this.$('form').find('input, select, textarea').first().focus().select();
this.$('form')
.find('input, select, textarea')
.first()
.focus()
.select();
}
onhide() {}

View File

@ -2,7 +2,7 @@ import MicroModal from 'micromodal';
import Component from '../Component';
import Modal from './Modal';
import {Vnode} from "mithril";
import { Vnode } from 'mithril';
/**
* The `ModalManager` component manages a modal dialog. Only one modal dialog
@ -48,7 +48,8 @@ export default class ModalManager extends Component {
m.redraw();
if (!$('.modal-backdrop').length) {
$('<div />').addClass('modal-backdrop')
$('<div />')
.addClass('modal-backdrop')
.appendTo('body');
}
@ -60,7 +61,7 @@ export default class ModalManager extends Component {
});
this.showing = false;
}
},
});
this.onready();

View File

@ -1,7 +1,7 @@
import Component, { ComponentProps } from '../Component';
export interface PlaceholderProps extends ComponentProps {
text: string
text: string;
}
/**

View File

@ -3,7 +3,7 @@ import {ComponentProps} from '../Component';
import RequestError from '../utils/RequestError';
export interface RequestErrorModalProps extends ComponentProps {
error: RequestError,
error: RequestError;
}
export default class RequestErrorModal<T extends RequestErrorModalProps = RequestErrorModalProps> extends Modal<T> {
@ -12,9 +12,7 @@ export default class RequestErrorModal<T extends RequestErrorModalProps = Reques
}
title(): string {
return this.props.error.xhr
? `${this.props.error.xhr.status} ${this.props.error.xhr.statusText}`
: '';
return this.props.error.xhr ? `${this.props.error.xhr.status} ${this.props.error.xhr.statusText}` : '';
}
content() {
@ -26,11 +24,15 @@ export default class RequestErrorModal<T extends RequestErrorModalProps = Reques
responseText = this.props.error.responseText;
}
return <div className="Modal-body">
return (
<div className="Modal-body">
<pre>
{this.props.error.options.method} {this.props.error.options.url}<br/><br/>
{this.props.error.options.method} {this.props.error.options.url}
<br />
<br />
{responseText}
</pre>
</div>
);
}
}

View File

@ -26,13 +26,10 @@ export default class SelectDropdown extends Dropdown<SelectDropdownProps> {
getButtonContent() {
const activeChild = this.props.children.filter(child => child.attrs.active)[0];
let label = activeChild && activeChild.attrs.children || this.props.defaultLabel;
let label = (activeChild && activeChild.attrs.children) || this.props.defaultLabel;
if (label instanceof Array) label = label[0];
return [
<span className="Button-label">{label}</span>,
icon(this.props.caretIcon, {className: 'Button-caret'})
];
return [<span className="Button-label">{label}</span>, icon(this.props.caretIcon, { className: 'Button-caret' })];
}
}

View File

@ -28,7 +28,9 @@ export default function highlight(string: string, phrase: string|RegExp, length?
// Convert the string into HTML entities, then highlight all matches with
// <mark> tags. Then we will return the result as a trusted HTML string.
highlighted = $('<div/>').text(highlighted).html();
highlighted = $('<div/>')
.text(highlighted)
.html();
if (phrase) highlighted = highlighted.replace(regexp, '<mark>$&</mark>');

View File

@ -39,16 +39,16 @@ export default function listItems(items) {
item.key = item.attrs.key;
}
const node = isListItem
? item
: <li className={classNames(className, [
(item.itemName && `item-${item.itemName}`),
active && 'active',
])}
key={item.attrs?.key || item.itemName}>
const node = isListItem ? (
item
) : (
<li
className={classNames(className, [item.itemName && `item-${item.itemName}`, active && 'active'])}
key={item.attrs?.key || item.itemName}
>
{item}
</li>;
</li>
);
node.state = node.state || {};

View File

@ -3,7 +3,7 @@ import computed from '../utils/computed';
import { getPlainContent } from '../utils/string';
import Discussion from './Discussion';
import User from "./User";
import User from './User';
export default class Post extends Model {
number = Model.attribute('number') as () => number;

View File

@ -46,7 +46,12 @@ export default class User extends Model {
}) as () => string;
isOnline(): boolean {
return this.lastSeenAt() > dayjs().subtract(5, 'minutes').toDate();
return (
this.lastSeenAt() >
dayjs()
.subtract(5, 'minutes')
.toDate()
);
}
/**

View File

@ -1,4 +1,4 @@
import Mithril from "mithril";
import Mithril from 'mithril';
export interface RequestErrorResponse extends JSON {
errors?: {

View File

@ -27,7 +27,10 @@ export default function computed(dependentKeys: string|string[], compute: Functi
});
if (recompute) {
computedValue = compute.apply(this, keys.map(key => dependentValues[key]));
computedValue = compute.apply(
this,
keys.map(key => dependentValues[key])
);
}
return computedValue;

View File

@ -23,11 +23,11 @@ export default function humanTime(time: Date): string {
if (m.year() === dayjs().year()) {
ago = m.format('D MMM');
} else {
ago = m.format('MMM \'YY');
ago = m.format("MMM 'YY");
}
} else {
ago = m.fromNow();
}
return ago;
};
}

View File

@ -42,13 +42,14 @@ export default () => {
return node;
};
Object.keys(mo).forEach(key => _m[key] = mo[key]);
Object.keys(mo).forEach(key => (_m[key] = mo[key]));
_m.withAttr = (key: string, cb: Function) => function () {
_m.withAttr = (key: string, cb: Function) =>
function() {
cb(this.getAttribute(key) || this[key]);
};
_m.prop = prop;
window['m'] = _m;
}
};

View File

@ -6,7 +6,7 @@ $.fn.tooltip = function (option) {
return this.each(function() {
const $this = $(this);
let data = $this.data('bs.tooltip');
const options = typeof option === 'object' && option || {};
const options = (typeof option === 'object' && option) || {};
if ($this.attr('title')) {
options.title = $this.attr('title');
@ -26,14 +26,14 @@ $.fn.tooltip = function (option) {
// add $.fn.outerWidth and $.fn.outerHeight
['width', 'height'].forEach(function(dimension) {
const Dimension = dimension.replace(/./, function(m) {
return m[0].toUpperCase()
return m[0].toUpperCase();
});
$.fn[`outer${Dimension}`] = function(margin) {
const elem = this;
if (elem) {
const sides = {'width': ['left', 'right'], 'height': ['top', 'bottom']};
const sides = { width: ['left', 'right'], height: ['top', 'bottom'] };
let size = elem[dimension]();
sides[dimension].forEach(function(side) {
@ -50,23 +50,21 @@ $.fn.tooltip = function (option) {
// allow use of $(':input')
// @ts-ignore
$.expr[':']['input'] = function() {
if (('disabled' in this) || ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(this.tagName)) return this;
if ('disabled' in this || ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(this.tagName)) return this;
};
// add $().hover() method
$.fn.hover = function(hover, leave) {
return this
.on('mouseenter', hover)
.on('mouseleave', leave || hover);
return this.on('mouseenter', hover).on('mouseleave', leave || hover);
};
// add animated scroll
$.fn.animatedScrollTop = function(to, duration = $.fx.speeds._default, callback) {
if (typeof to === 'number') to -= (window.scrollY || window.pageYOffset);
if (typeof to === 'number') to -= window.scrollY || window.pageYOffset;
jump(to, {
duration: $.fx.speeds[duration] || duration,
callback
callback,
});
return this;
@ -106,7 +104,6 @@ $.fn.bind = function(eventName, data, callback) {
/// intercept and replace the special event handler to add functionality
specialEvent.originalHandler = specialEvent.handler;
specialEvent.handler = function() {
/// make event argument writable, like on jQuery
const args = Array.prototype.slice.call(arguments);
@ -114,7 +111,6 @@ $.fn.bind = function(eventName, data, callback) {
/// define the event handle, $.event.dispatch is only for newer versions of jQuery
$.event.handle = function() {
/// make context of trigger the event element
const args = Array.prototype.slice.call(arguments);
const event = args[0];
@ -124,7 +120,7 @@ $.fn.bind = function(eventName, data, callback) {
};
specialEvent.originalHandler.apply(this, args);
}
};
}
/// setup special events on Zepto

View File

@ -2,9 +2,7 @@
* Truncate a string to the given length, appending ellipses if necessary.
*/
export function truncate(string: string, length: number, start = 0): string {
return (start > 0 ? '...' : '') +
string.substring(start, start + length) +
(string.length > start + length ? '...' : '');
return (start > 0 ? '...' : '') + string.substring(start, start + length) + (string.length > start + length ? '...' : '');
}
/**
@ -12,7 +10,8 @@ export function truncate(string: string, length: number, start = 0): string {
* converted to hyphens.
*/
export function slug(string: string): string {
return string.toLowerCase()
return string
.toLowerCase()
.replace(/[^a-z0-9]/gi, '-')
.replace(/-+/g, '-')
.replace(/-$|^-/g, '');
@ -23,15 +22,16 @@ export function slug(string: string): string {
* meaningful punctuation.
*/
export function getPlainContent(string: string): string {
const html = string
.replace(/(<\/p>|<br>)/g, '$1 &nbsp;')
.replace(/<img\b[^>]*>/ig, ' ');
const html = string.replace(/(<\/p>|<br>)/g, '$1 &nbsp;').replace(/<img\b[^>]*>/gi, ' ');
const dom = $('<div/>').html(html);
dom.find(getPlainContent.removeSelectors.join(',')).remove();
return dom.text().replace(/\s+/g, ' ').trim();
return dom
.text()
.replace(/\s+/g, ' ')
.trim();
}
/**

View File

@ -10,18 +10,42 @@ export function hsvToRgb(h: number, s: number, v: number) {
const 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;
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)
b: Math.floor(b * 255),
};
}

View File

@ -7,22 +7,22 @@ import HeaderSecondary from './components/HeaderSecondary';
import Page from './components/Page';
import IndexPage from './components/IndexPage';
import PostsUserPage from './components/PostsUserPage';
import User from "../common/models/User";
import Post from "../common/models/Post";
import Discussion from "../common/models/Discussion";
import User from '../common/models/User';
import Post from '../common/models/Post';
import Discussion from '../common/models/Discussion';
export default class Forum extends Application {
routes = {
'index': { path: '/all', component: IndexPage },
index: { path: '/all', component: IndexPage },
'discussion': { path: '/d/:id', component: IndexPage },
discussion: { path: '/d/:id', component: IndexPage },
'discussion.near': { path: '/d/:id/:near', component: IndexPage },
'user': { path: '/u/:username', component: PostsUserPage },
user: { path: '/u/:username', component: PostsUserPage },
'user.posts': { path: '/u/:username', component: PostsUserPage },
'user.discussions': { path: '/u/:username/discussions', component: PostsUserPage },
'settings': { path: '/settings', component: PostsUserPage },
settings: { path: '/settings', component: PostsUserPage },
'index.filter': { path: '/:filter', component: IndexPage },
};
@ -86,7 +86,7 @@ export default class Forum extends Application {
const slug = discussion.slug();
return this.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
id: discussion.id() + (slug.trim() ? '-' + slug : ''),
near: near && near !== 1 ? near : undefined
near: near && near !== 1 ? near : undefined,
});
};
@ -102,7 +102,7 @@ export default class Forum extends Application {
*/
this.route.user = (user: User): string => {
return this.route('user', {
username: user.username()
username: user.username(),
});
};
}

View File

@ -36,9 +36,14 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
const user = this.props.user;
return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
<div
className={
'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')
}
>
{avatar(user)}
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
<a
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
@ -46,12 +51,11 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
ondragenter={this.enableDragover.bind(this)}
ondragleave={this.disableDragover.bind(this)}
ondragend={this.disableDragover.bind(this)}
ondrop={this.dropUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))}
ondrop={this.dropUpload.bind(this)}
>
{this.loading ? LoadingIndicator.component() : user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle')}
</a>
<ul className="Dropdown-menu Menu">
{listItems(this.controlItems().toArray())}
</ul>
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
</div>
);
}
@ -64,19 +68,21 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
controlItems() {
const items = new ItemList();
items.add('upload',
items.add(
'upload',
Button.component({
icon: 'fas fa-upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.openPicker.bind(this)
onclick: this.openPicker.bind(this),
})
);
items.add('remove',
items.add(
'remove',
Button.component({
icon: 'fas fa-times',
children: app.translator.trans('core.forum.user.avatar_remove_button'),
onclick: this.remove.bind(this)
onclick: this.remove.bind(this),
})
);
@ -144,7 +150,11 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
const user = this.props.user;
const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => {
$input
.appendTo('body')
.hide()
.click()
.on('change', e => {
this.upload($(e.target)[0].files[0]);
});
}
@ -168,11 +178,8 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
method: 'POST',
url: `${app.forum.attribute('apiUrl')}/users/${user.id()}/avatar`,
serialize: raw => raw,
body
}).then(
this.success.bind(this),
this.failure.bind(this)
);
body,
}).then(this.success.bind(this), this.failure.bind(this));
}
/**
@ -186,11 +193,8 @@ export default class AvatarEditor extends Component<AvatarEditorProps> {
app.request({
method: 'DELETE',
url: `${app.forum.attribute('apiUrl')}/users/${user.id()}/avatar`
}).then(
this.success.bind(this),
this.failure.bind(this)
);
url: `${app.forum.attribute('apiUrl')}/users/${user.id()}/avatar`,
}).then(this.success.bind(this), this.failure.bind(this));
}
/**

View File

@ -38,13 +38,17 @@ export default class CommentPost extends Post {
// Note: we avoid using JSX for the <ul> below because it results in some
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
// be reverted when we upgrade to Mithril 1.0.
return super.content().concat([
return super
.content()
.concat([
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
<div className="Post-body">
{this.isEditing()
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
: m.trust(this.props.post.contentHtml())}
</div>
{this.isEditing() ? (
<div className="Post-preview" config={this.configPreview.bind(this)} />
) : (
m.trust(this.props.post.contentHtml())
)}
</div>,
]);
}
@ -77,12 +81,15 @@ export default class CommentPost extends Post {
const post = this.props.post;
const attrs = super.attrs();
attrs.className = (attrs.className || '') + ' ' + classNames({
'CommentPost': true,
attrs.className =
(attrs.className || '') +
' ' +
classNames({
CommentPost: true,
'Post--hidden': post.isHidden(),
'Post--edited': post.isEdited(),
'revealContent': this.revealContent,
'editing': this.isEditing()
revealContent: this.revealContent,
editing: this.isEditing(),
});
return attrs;
@ -137,13 +144,14 @@ export default class CommentPost extends Post {
// If the post is hidden, add a button that allows toggling the visibility
// of the post's content.
if (post.isHidden()) {
items.add('toggle', (
items.add(
'toggle',
Button.component({
className: 'Button Button--default Button--more',
icon: 'fas fa-ellipsis-h',
onclick: this.toggleContent.bind(this)
onclick: this.toggleContent.bind(this),
})
));
);
}
return items;

View File

@ -1,7 +1,7 @@
import highlight from '../../common/helpers/highlight';
import LinkButton from '../../common/components/LinkButton';
import SearchSource from "./SearchSource";
import Discussion from "../../common/models/Discussion";
import SearchSource from './SearchSource';
import Discussion from '../../common/models/Discussion';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
@ -18,10 +18,10 @@ export default class DiscussionsSearchSource extends SearchSource {
const params = {
filter: { q: query },
page: { limit: 3 },
include: 'mostRelevantPost'
include: 'mostRelevantPost',
};
return app.store.find<Discussion>('discussions', params).then(results => this.results[query] = results);
return app.store.find<Discussion>('discussions', params).then(results => (this.results[query] = results));
}
view(query: string) {
@ -35,7 +35,7 @@ export default class DiscussionsSearchSource extends SearchSource {
{LinkButton.component({
icon: 'fas fa-search',
children: app.translator.trans('core.forum.search.all_discussions_button', { query }),
href: app.route('index', {q: query})
href: app.route('index', { q: query }),
})}
</li>,
results.map(discussion => {
@ -45,11 +45,15 @@ export default class DiscussionsSearchSource extends SearchSource {
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<m.route.Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
{mostRelevantPost ? (
<div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div>
) : (
''
)}
</m.route.Link>
</li>
);
})
}),
];
}
}

View File

@ -8,11 +8,7 @@ import listItems from '../../common/helpers/listItems';
*/
export default class HeaderPrimary extends Component {
view() {
return (
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}
/**

View File

@ -17,11 +17,7 @@ import Search from './Search';
*/
export default class HeaderSecondary extends Component {
view() {
return (
<ul className="Header-controls">
{listItems(this.items().toArray())}
</ul>
);
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
}
/**
@ -32,11 +28,12 @@ export default class HeaderSecondary extends Component {
items.add('search', Search.component(), 30);
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) {
const locales = [];
for (const locale in app.data.locales) {
locales.push(Button.component({
locales.push(
Button.component({
active: app.data.locale === locale,
children: app.data.locales[locale],
icon: app.data.locale === locale ? 'fas fa-check' : true,
@ -47,14 +44,19 @@ export default class HeaderSecondary extends Component {
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
window.location.reload();
}
}
}));
},
})
);
}
items.add('locale', SelectDropdown.component({
items.add(
'locale',
SelectDropdown.component({
children: locales,
buttonClassName: 'Button Button--link'
}), 20);
buttonClassName: 'Button Button--link',
}),
20
);
}
if (app.session.user) {
@ -62,21 +64,25 @@ export default class HeaderSecondary extends Component {
items.add('session', SessionDropdown.component(), 0);
} else {
if (app.forum.attribute('allowSignUp')) {
items.add('signUp',
items.add(
'signUp',
Button.component({
children: app.translator.trans('core.forum.header.sign_up_link'),
className: 'Button Button--link',
onclick: () => app.modal.show(new SignUpModal())
}), 10
onclick: () => app.modal.show(new SignUpModal()),
}),
10
);
}
items.add('logIn',
items.add(
'logIn',
Button.component({
children: app.translator.trans('core.forum.header.log_in_link'),
className: 'Button Button--link',
onclick: () => app.modal.show(new LogInModal())
}), 0
onclick: () => app.modal.show(new LogInModal()),
}),
0
);
}

View File

@ -6,9 +6,7 @@ import ItemList from '../../common/utils/ItemList';
*/
export default class LogInButtons extends Component {
view() {
return <div className="LogInButtons">
{this.items().toArray()}
</div>
return <div className="LogInButtons">{this.items().toArray()}</div>;
}
/**

View File

@ -2,8 +2,8 @@ import Stream from 'mithril/stream';
import { ComponentProps } from '../../common/Component';
import Modal from '../../common/components/Modal';
import ItemList from "../../common/utils/ItemList";
import Button from "../../common/components/Button";
import ItemList from '../../common/utils/ItemList';
import Button from '../../common/components/Button';
import LogInButtons from './LogInButtons';
@ -51,58 +51,71 @@ export default class LogInModal extends Modal<LogInModalProps> {
}
content() {
return [
<div className="Modal-body">
{this.body()}
</div>,
<div className="Modal-footer">
{this.footer()}
</div>
];
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
}
body() {
return [
<LogInButtons/>,
<div className="Form Form--centered">
{this.fields().toArray()}
</div>
];
return [<LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
}
fields() {
const items = new ItemList();
items.add('identification', <div className="Form-group">
<input className="FormControl" name="identification" type="text" placeholder={app.translator.transText('core.forum.log_in.username_or_email_placeholder')}
items.add(
'identification',
<div className="Form-group">
<input
className="FormControl"
name="identification"
type="text"
placeholder={app.translator.transText('core.forum.log_in.username_or_email_placeholder')}
bidi={this.identification}
disabled={this.loading} />
</div>, 30);
disabled={this.loading}
/>
</div>,
30
);
items.add('password', <div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={app.translator.transText('core.forum.log_in.password_placeholder')}
items.add(
'password',
<div className="Form-group">
<input
className="FormControl"
name="password"
type="password"
placeholder={app.translator.transText('core.forum.log_in.password_placeholder')}
bidi={this.password}
disabled={this.loading} />
</div>, 20);
disabled={this.loading}
/>
</div>,
20
);
items.add('remember', <div className="Form-group">
items.add(
'remember',
<div className="Form-group">
<div>
<label className="checkbox">
<input type="checkbox" bidi={this.remember} disabled={this.loading} />
{app.translator.trans('core.forum.log_in.remember_me_label')}
</label>
</div>
</div>, 10);
</div>,
10
);
items.add('submit', <div className="Form-group">
items.add(
'submit',
<div className="Form-group">
{Button.component({
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.log_in.submit_button')
children: app.translator.trans('core.forum.log_in.submit_button'),
})}
</div>, -10);
</div>,
-10
);
return items;
}
@ -117,7 +130,7 @@ export default class LogInModal extends Modal<LogInModalProps> {
<p className="LogInModal-signUp">
{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}
</p>
)
),
];
}
@ -163,11 +176,9 @@ export default class LogInModal extends Modal<LogInModalProps> {
const password = this.password();
const remember = this.remember();
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
.then(
() => window.location.reload(),
this.loaded.bind(this)
);
app.session
.login({ identification, password, remember }, { errorHandler: this.onerror.bind(this) })
.then(() => window.location.reload(), this.loaded.bind(this));
}
onerror(error) {

View File

@ -34,7 +34,7 @@ export default class NotificationList extends Component {
className: 'Button Button--icon Button--link',
icon: 'fas fa-check',
title: app.translator.transText('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: this.markAllAsRead.bind(this)
onclick: this.markAllAsRead.bind(this),
})}
</div>
@ -42,7 +42,8 @@ export default class NotificationList extends Component {
</div>
<div className="NotificationList-content">
{pages.length ? pages.map(notifications => {
{pages.length
? pages.map(notifications => {
const groups = [];
const discussions = {};
@ -74,17 +75,17 @@ export default class NotificationList extends Component {
return (
<div className="NotificationGroup">
{group.discussion
? (
<m.route.Link className="NotificationGroup-header"
href={app.route.discussion(group.discussion)}>
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
{group.discussion ? (
<m.route.Link className="NotificationGroup-header" href={app.route.discussion(group.discussion)}>
{badges && badges.length ? (
<ul className="NotificationGroup-badges badges">{listItems(badges)}</ul>
) : (
''
)}
{group.discussion.title()}
</m.route.Link>
) : (
<div className="NotificationGroup-header">
{app.forum.attribute('title')}
</div>
<div className="NotificationGroup-header">{app.forum.attribute('title')}</div>
)}
<ul className="NotificationGroup-content">
@ -96,10 +97,15 @@ export default class NotificationList extends Component {
</div>
);
});
}) : ''}
{this.loading
? <LoadingIndicator className="LoadingIndicator--block" />
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
})
: ''}
{this.loading ? (
<LoadingIndicator className="LoadingIndicator--block" />
) : pages.length ? (
''
) : (
<div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
)}
</div>
</div>
);
@ -109,7 +115,7 @@ export default class NotificationList extends Component {
super.oncreate(vnode);
const $notifications = this.$('.NotificationList-content');
const $scrollParent = this.$scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
const $scrollParent = (this.$scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window));
this.scrollHandler = () => {
const scrollTop = $scrollParent.scrollTop();
@ -158,7 +164,8 @@ export default class NotificationList extends Component {
const params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null;
return app.store.find<Notification>('notifications', params)
return app.store
.find<Notification>('notifications', params)
.then(this.parseResults.bind(this))
.catch(() => {})
.then(() => {
@ -189,12 +196,12 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({ unreadNotificationCount: 0 });
app.cache.notifications.forEach(notifications => {
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
notifications.forEach(notification => notification.pushAttributes({ isRead: true }));
});
app.request({
url: `${app.forum.attribute('apiUrl')}/notifications/read`,
method: 'POST'
method: 'POST',
});
}
}

View File

@ -21,7 +21,7 @@ export default class NotificationsDropdown extends Dropdown {
vdom.attrs.title = this.props.label;
vdom.attrs.className += (newNotifications ? ' new' : '');
vdom.attrs.className += newNotifications ? ' new' : '';
vdom.attrs.onclick = this.onclick.bind(this);
return vdom;
@ -33,7 +33,7 @@ export default class NotificationsDropdown extends Dropdown {
return [
icon(this.props.icon, { className: 'Button-icon' }),
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
<span className="Button-label">{this.props.label}</span>
<span className="Button-label">{this.props.label}</span>,
];
}

View File

@ -3,11 +3,11 @@ import Dropdown from '../../common/components/Dropdown';
import PostControls from '../utils/PostControls';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import SubtreeRetainer from "../../common/utils/SubtreeRetainer";
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import PostModel from '../../common/models/Post';
export interface PostProps extends ComponentProps {
post: PostModel
post: PostModel;
}
/**
@ -53,20 +53,27 @@ export default class Post<T extends PostProps = PostProps> extends Component<Pos
<aside className="Post-actions">
<ul>
{listItems(this.actionItems().toArray())}
{controls.length ? <li>
{controls.length ? (
<li>
<Dropdown
className="Post-controls"
buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right"
icon="fas fa-ellipsis-h"
onshow={() => this.$('.Post-actions').addClass('open')}
onhide={() => this.$('.Post-actions').removeClass('open')}>
onhide={() => this.$('.Post-actions').removeClass('open')}
>
{controls}
</Dropdown>
</li> : ''}
</li>
) : (
''
)}
</ul>
</aside>
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>
<footer className="Post-footer">
<ul>{listItems(this.footerItems().toArray())}</ul>
</footer>
</div>
);
</article>

View File

@ -4,8 +4,8 @@ import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import userOnline from '../../common/helpers/userOnline';
import listItems from '../../common/helpers/listItems';
import {PostProps} from "./Post";
import LinkButton from "../../common/components/LinkButton";
import { PostProps } from './Post';
import LinkButton from '../../common/components/LinkButton';
/**
* The `PostUser` component shows the avatar and username of a post's author.
@ -23,7 +23,9 @@ export default class PostUser extends Component<PostProps> {
if (!user) {
return (
<div className="PostUser">
<h3>{avatar(user, {className: 'PostUser-avatar'})} {username(user)}</h3>
<h3>
{avatar(user, { className: 'PostUser-avatar' })} {username(user)}
</h3>
</div>
);
}
@ -34,7 +36,7 @@ export default class PostUser extends Component<PostProps> {
card = UserCard.component({
user,
className: 'UserCard--popover',
controlsButtonClassName: 'Button Button--icon Button--flat'
controlsButtonClassName: 'Button Button--icon Button--flat',
});
}
@ -47,9 +49,7 @@ export default class PostUser extends Component<PostProps> {
{username(user)}
</LinkButton>
</h3>
<ul className="PostUser-badges badges">
{listItems(user.badges().toArray())}
</ul>
<ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul>
{card}
</div>
);
@ -86,7 +86,8 @@ export default class PostUser extends Component<PostProps> {
* Hide the user card.
*/
hideCard() {
this.$('.UserCard').removeClass('in')
this.$('.UserCard')
.removeClass('in')
.one('transitionend webkitTransitionEnd oTransitionEnd', () => {
this.cardVisible = false;
m.redraw();

View File

@ -61,7 +61,7 @@ export default class PostsUserPage extends UserPage {
{Button.component({
children: app.translator.trans('core.forum.user.posts_load_more_button'),
className: 'Button',
onclick: this.loadMore.bind(this)
onclick: this.loadMore.bind(this),
})}
</div>
);
@ -73,15 +73,15 @@ export default class PostsUserPage extends UserPage {
{this.posts.map(post => (
<li>
<div className="PostsUserPage-discussion">
{app.translator.trans('core.forum.user.in_discussion_text', {discussion: <m.route.Link href={app.route.post(post)}>{post.discussion().title()}</m.route.Link>})}
{app.translator.trans('core.forum.user.in_discussion_text', {
discussion: <m.route.Link href={app.route.post(post)}>{post.discussion().title()}</m.route.Link>,
})}
</div>
{CommentPost.component({ post })}
</li>
))}
</ul>
<div className="PostsUserPage-loadMore">
{footer}
</div>
<div className="PostsUserPage-loadMore">{footer}</div>
</div>
);
}
@ -117,10 +117,10 @@ export default class PostsUserPage extends UserPage {
return app.store.find<Post>('posts', {
filter: {
user: this.user.id(),
type: 'comment'
type: 'comment',
},
page: { offset, limit: this.loadLimit },
sort: '-createdAt'
sort: '-createdAt',
});
}

View File

@ -75,30 +75,39 @@ export default class Search extends Component {
if (!this.sources.length) return <div />;
return (
<div className={'Search ' + classNames({
<div
className={
'Search ' +
classNames({
open: this.value() && this.hasFocus,
focused: this.hasFocus,
active: !!currentSearch,
loading: !!this.loadingSources
})}>
loading: !!this.loadingSources,
})
}
>
<div className="Search-input">
<input className="FormControl"
<input
className="FormControl"
type="search"
placeholder={app.translator.transText('core.forum.header.search_placeholder')}
value={this.value()}
oninput={m.withAttr('value', this.value)}
onfocus={() => this.hasFocus = true}
onblur={() => this.hasFocus = false}/>
{this.loadingSources
? LoadingIndicator.component({size: 'tiny', className: 'Button Button--icon Button--link'})
: currentSearch
? <button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>{icon('fas fa-times-circle')}</button>
: ''}
onfocus={() => (this.hasFocus = true)}
onblur={() => (this.hasFocus = false)}
/>
{this.loadingSources ? (
LoadingIndicator.component({ size: 'tiny', className: 'Button Button--icon Button--link' })
) : currentSearch ? (
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
{icon('fas fa-times-circle')}
</button>
) : (
''
)}
</div>
<ul className="Dropdown-menu Search-results">
{this.value() && this.hasFocus
? this.sources.map(source => source.view(this.value()))
: ''}
{this.value() && this.hasFocus ? this.sources.map(source => source.view(this.value())) : ''}
</ul>
</div>
);
@ -118,9 +127,7 @@ export default class Search extends Component {
// Whenever the mouse is hovered over a search result, highlight it.
.on('mouseenter', '> li:not(.Dropdown-header)', function() {
search.setIndex(
search.selectableItems().index(this)
);
search.setIndex(search.selectableItems().index(this));
});
const $input = this.$('input');
@ -164,7 +171,9 @@ export default class Search extends Component {
})
.on('focus', function() {
$(this).one('mouseup', e => e.preventDefault()).select();
$(this)
.one('mouseup', e => e.preventDefault())
.select();
});
}
@ -185,7 +194,11 @@ export default class Search extends Component {
this.loadingSources = 0;
if (this.value()) {
m.route.set(this.getItem(this.index).find('a').attr('href'));
m.route.set(
this.getItem(this.index)
.find('a')
.attr('href')
);
} else {
this.clear();
}
@ -235,9 +248,7 @@ export default class Search extends Component {
* @return {Integer}
*/
getCurrentNumericIndex() {
return this.selectableItems().index(
this.getItem(this.index)
);
return this.selectableItems().index(this.getItem(this.index));
}
/**
@ -276,7 +287,10 @@ export default class Search extends Component {
fixedIndex = 0;
}
const $item = $items.removeClass('active').eq(fixedIndex).addClass('active');
const $item = $items
.removeClass('active')
.eq(fixedIndex)
.addClass('active');
this.index = $item.attr('data-index') || fixedIndex;

View File

@ -29,10 +29,7 @@ export default class SessionDropdown extends Dropdown {
getButtonContent() {
const user = app.session.user;
return [
avatar(user), ' ',
<span className="Button-label">{username(user)}</span>
];
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
}
/**
@ -42,26 +39,29 @@ export default class SessionDropdown extends Dropdown {
const items = new ItemList();
const user = app.session.user;
items.add('profile',
items.add(
'profile',
LinkButton.component({
icon: 'fas fa-user',
children: app.translator.trans('core.forum.header.profile_button'),
href: app.route.user(user)
href: app.route.user(user),
}),
100
);
items.add('settings',
items.add(
'settings',
LinkButton.component({
icon: 'fas fa-cog',
children: app.translator.trans('core.forum.header.settings_button'),
href: app.route('settings')
href: app.route('settings'),
}),
50
);
if (app.forum.attribute('adminUrl')) {
items.add('administration',
items.add(
'administration',
LinkButton.component({
icon: 'fas fa-wrench',
children: app.translator.trans('core.forum.header.admin_button'),
@ -74,11 +74,12 @@ export default class SessionDropdown extends Dropdown {
items.add('separator', Separator.component(), -90);
items.add('logOut',
items.add(
'logOut',
Button.component({
icon: 'fas fa-sign-out-alt',
children: app.translator.trans('core.forum.header.log_out_button'),
onclick: app.session.logout.bind(app.session)
onclick: app.session.logout.bind(app.session),
}),
-100
);

View File

@ -8,7 +8,7 @@ import icon from '../../common/helpers/icon';
import Dropdown from '../../common/components/Dropdown';
import AvatarEditor from './AvatarEditor';
import listItems from '../../common/helpers/listItems';
import User from "../../common/models/User";
import User from '../../common/models/User';
export interface UserCardProps extends ComponentProps {
user: User;
@ -29,25 +29,25 @@ export default class UserCard extends Component<UserCardProps> {
const badges = user.badges().toArray();
return (
<div className={'UserCard ' + (this.props.className || '')}
style={color ? {backgroundColor: color} : ''}>
<div className={'UserCard ' + (this.props.className || '')} style={color ? { backgroundColor: color } : ''}>
<div className="darkenBackground">
<div className="container">
{controls.length ? Dropdown.component({
{controls.length
? Dropdown.component({
children: controls,
className: 'UserCard-controls App-primaryControl',
menuClassName: 'Dropdown-menu--right',
buttonClassName: this.props.controlsButtonClassName,
label: app.translator.trans('core.forum.user_controls.button'),
icon: 'fas fa-ellipsis-v'
}) : ''}
icon: 'fas fa-ellipsis-v',
})
: ''}
<div className="UserCard-profile">
<h2 className="UserCard-identity">
{this.props.editable
? [AvatarEditor.component({user, className: 'UserCard-avatar'}), username(user)]
: (
{this.props.editable ? (
[AvatarEditor.component({ user, className: 'UserCard-avatar' }), username(user)]
) : (
<m.route.Link href={app.route.user(user)}>
<div className="UserCard-avatar">{avatar(user)}</div>
{username(user)}
@ -55,15 +55,9 @@ export default class UserCard extends Component<UserCardProps> {
)}
</h2>
{badges.length ? (
<ul className="UserCard-badges badges">
{listItems(badges)}
</ul>
) : ''}
{badges.length ? <ul className="UserCard-badges badges">{listItems(badges)}</ul> : ''}
<ul className="UserCard-info">
{listItems(this.infoItems().toArray())}
</ul>
<ul className="UserCard-info">{listItems(this.infoItems().toArray())}</ul>
</div>
</div>
</div>
@ -84,13 +78,14 @@ export default class UserCard extends Component<UserCardProps> {
if (lastSeenAt) {
const online = user.isOnline();
items.add('lastSeen', (
items.add(
'lastSeen',
<span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
{online
? [icon('fas fa-circle'), ' ', app.translator.trans('core.forum.user.online_text')]
: [icon('far fa-clock'), ' ', humanTime(lastSeenAt)]}
</span>
));
);
}
items.add('joined', app.translator.trans('core.forum.user.joined_date_text', { ago: humanTime(user.joinTime()) }));

View File

@ -30,26 +30,24 @@ export default abstract class UserPage extends Page {
view() {
return (
<div className="UserPage">
{this.user ? [
{this.user
? [
UserCard.component({
user: this.user,
className: 'Hero UserHero',
editable: this.user.canEdit() || this.user === app.session.user,
controlsButtonClassName: 'Button'
controlsButtonClassName: 'Button',
}),
<div className="container">
<div className="sideNavContainer">
<nav className="sideNav UserPage-nav" config={affixSidebar}>
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
<div className="sideNavOffset UserPage-content">
{this.content()}
<div className="sideNavOffset UserPage-content">{this.content()}</div>
</div>
</div>
</div>
] : [
LoadingIndicator.component({lassName: 'LoadingIndicator--block'})
]}
</div>,
]
: [LoadingIndicator.component({ lassName: 'LoadingIndicator--block' })]}
</div>
);
}
@ -105,11 +103,12 @@ export default abstract class UserPage extends Page {
sidebarItems() {
const items = new ItemList();
items.add('nav',
items.add(
'nav',
SelectDropdown.component({
children: this.navItems().toArray(),
className: 'App-titleControl',
buttonClassName: 'Button'
buttonClassName: 'Button',
})
);
@ -123,31 +122,34 @@ export default abstract class UserPage extends Page {
const items = new ItemList();
const user = this.user;
items.add('posts',
items.add(
'posts',
LinkButton.component({
href: app.route('user.posts', { username: user.username() }),
children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentCount()}</span>],
icon: 'far fa-comment'
icon: 'far fa-comment',
}),
100
);
items.add('discussions',
items.add(
'discussions',
LinkButton.component({
href: app.route('user.discussions', { username: user.username() }),
children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionCount()}</span>],
icon: 'fas fa-bars'
icon: 'fas fa-bars',
}),
90
);
if (app.session.user === user) {
items.add('separator', Separator.component(), -90);
items.add('settings',
items.add(
'settings',
LinkButton.component({
href: app.route('settings'),
children: app.translator.trans('core.forum.user.settings_link'),
icon: 'fas fa-cog'
icon: 'fas fa-cog',
}),
-100
);

View File

@ -14,10 +14,12 @@ export default class UsersSearchSource extends SearchSource {
protected results: { [key: string]: User[] } = {};
search(query: string) {
return app.store.find<User>('users', {
return app.store
.find<User>('users', {
filter: { q: query },
page: {limit: 5}
}).then(results => {
page: { limit: 5 },
})
.then(results => {
this.results[query] = results;
m.redraw();
});
@ -27,7 +29,11 @@ export default class UsersSearchSource extends SearchSource {
query = query.toLowerCase();
const results = (this.results[query] || [])
.concat(app.store.all<User>('users').filter(user => [user.username(), user.displayName()].some(value => value.toLowerCase().substr(0, query.length) === query)))
.concat(
app.store
.all<User>('users')
.filter(user => [user.username(), user.displayName()].some(value => value.toLowerCase().substr(0, query.length) === query))
)
.filter((e, i, arr) => arr.lastIndexOf(e) === i)
.sort((a, b) => a.displayName().localeCompare(b.displayName()));
@ -53,7 +59,7 @@ export default class UsersSearchSource extends SearchSource {
</m.route.Link>
</li>
);
})
}),
];
}
}

View File

@ -1,11 +1,11 @@
import {Vnode} from "mithril";
import { Vnode } from 'mithril';
// import EditPostComposer from '../components/EditPostComposer';
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
import ItemList from '../../common/utils/ItemList';
import Post from "../../common/models/Post";
import PostComponent from "../../forum/components/Post";
import Post from '../../common/models/Post';
import PostComponent from '../../forum/components/Post';
/**
* The `PostControls` utility constructs a list of buttons for a post which
@ -60,10 +60,16 @@ export default {
if (post.contentType() === 'comment' && post.canEdit()) {
if (!post.isHidden()) {
items.add('edit', Button.component({
items.add(
'edit',
Button.component(
{
icon: 'fas fa-pencil-alt',
onclick: this.editAction.bind(post)
}, app.translator.trans('core.forum.post_controls.edit_button')));
onclick: this.editAction.bind(post),
},
app.translator.trans('core.forum.post_controls.edit_button')
)
);
}
}
@ -83,26 +89,35 @@ export default {
if (post.contentType() === 'comment' && !post.isHidden()) {
if (post.canHide()) {
items.add('hide', Button.component({
items.add(
'hide',
Button.component({
icon: 'far fa-trash-alt',
children: app.translator.trans('core.forum.post_controls.delete_button'),
onclick: this.hideAction.bind(post)
}));
onclick: this.hideAction.bind(post),
})
);
}
} else {
if (post.contentType() === 'comment' && post.canHide()) {
items.add('restore', Button.component({
items.add(
'restore',
Button.component({
icon: 'fas fa-reply',
children: app.translator.trans('core.forum.post_controls.restore_button'),
onclick: this.restoreAction.bind(post)
}));
onclick: this.restoreAction.bind(post),
})
);
}
if (post.canDelete()) {
items.add('delete', Button.component({
items.add(
'delete',
Button.component({
icon: 'fas fa-times',
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
onclick: this.deleteAction.bind(post, context)
}));
onclick: this.deleteAction.bind(post, context),
})
);
}
}
@ -171,5 +186,5 @@ export default {
if (context) context.loading = false;
m.redraw();
});
}
},
};

View File

@ -4,7 +4,7 @@ import Separator from '../../common/components/Separator';
// import EditUserModal from '../components/EditUserModal';
import UserPage from '../components/UserPage';
import ItemList from '../../common/utils/ItemList';
import User from "../../common/models/User";
import User from '../../common/models/User';
/**
* The `UserControls` utility constructs a list of buttons for a user which
@ -46,11 +46,14 @@ export default {
const items = new ItemList();
if (user.canEdit()) {
items.add('edit', Button.component({
items.add(
'edit',
Button.component({
icon: 'fas fa-pencil-alt',
children: app.translator.trans('core.forum.user_controls.edit_button'),
onclick: this.editAction.bind(this, user)
}));
onclick: this.editAction.bind(this, user),
})
);
}
return items;
@ -63,11 +66,14 @@ export default {
const items = new ItemList();
if (user.id() !== '1' && user.canDelete()) {
items.add('delete', Button.component({
items.add(
'delete',
Button.component({
icon: 'fas fa-times',
children: app.translator.trans('core.forum.user_controls.delete_button'),
onclick: this.deleteAction.bind(this, user)
}));
onclick: this.deleteAction.bind(this, user),
})
);
}
return items;
@ -81,14 +87,16 @@ export default {
return;
}
user.delete().then(() => {
user.delete()
.then(() => {
this.showDeletionAlert(user, 'success');
if (app.current instanceof UserPage && app.current.user === user) {
app.history.back();
} else {
window.location.reload();
}
}).catch(() => this.showDeletionAlert(user, 'error'));
})
.catch(() => this.showDeletionAlert(user, 'error'));
},
/**
@ -98,12 +106,12 @@ export default {
const { username, email } = user.data.attributes;
const message = `core.forum.user_controls.delete_${type}_message`;
app.alerts.show(Alert.component({
app.alerts.show(
Alert.component({
type,
children: app.translator.trans(
message, { username, email }
)
}));
children: app.translator.trans(message, { username, email }),
})
);
},
/**
@ -111,5 +119,5 @@ export default {
*/
editAction(user: User) {
app.modal.show(new EditUserModal({ user }));
}
},
};

View File

@ -4,9 +4,21 @@ const merge = require('webpack-merge');
module.exports = merge(config(), {
output: {
library: 'flarum.core'
library: 'flarum.core',
},
// use zepto instead of jquery
module: {
rules: [
{
test: require.resolve('zepto'),
use: 'imports-loader?this=>window',
},
],
},
resolve: {
alias: {
jquery: 'zepto',
},
},
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
});