mirror of
https://github.com/flarum/framework.git
synced 2025-05-23 07:09:57 +08:00
Massive JavaScript cleanup
- Use JSX for templates - Docblock/comment everything - Mostly passes ESLint (still some work to do) - Lots of renaming, refactoring, etc. CSS hasn't been updated yet.
This commit is contained in:
55
js/lib/components/Button.js
Normal file
55
js/lib/components/Button.js
Normal file
@ -0,0 +1,55 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
/**
|
||||
* The `Button` component defines an element which, when clicked, performs an
|
||||
* action. The button may have the following special props:
|
||||
*
|
||||
* - `icon` The name of the icon class. If specified, the button will be given a
|
||||
* 'has-icon' class name.
|
||||
* - `disabled` Whether or not the button is disabled. If truthy, the button
|
||||
* will be given a 'disabled' class name, and any `onclick` handler will be
|
||||
* removed.
|
||||
*
|
||||
* All other props will be assigned as attributes on the button element.
|
||||
*
|
||||
* Note that a Button has no default class names. This is because a Button can
|
||||
* be used to represent any generic clickable control, like a menu item.
|
||||
*/
|
||||
export default class Button extends Component {
|
||||
view() {
|
||||
const attrs = Object.assign({}, this.props);
|
||||
|
||||
delete attrs.children;
|
||||
|
||||
attrs.className = (attrs.className || '');
|
||||
attrs.href = attrs.href || 'javascript:;';
|
||||
|
||||
const iconName = extract(attrs, 'icon');
|
||||
if (iconName) attrs.className += ' has-icon';
|
||||
|
||||
const disabled = extract(attrs, 'disabled');
|
||||
if (disabled) {
|
||||
attrs.className += ' disabled';
|
||||
delete attrs.onclick;
|
||||
}
|
||||
|
||||
return <a {...attrs}>{this.getButtonContent()}</a>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button's content.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
getButtonContent() {
|
||||
const iconName = this.props.icon;
|
||||
|
||||
return [
|
||||
iconName ? icon(iconName) : '',
|
||||
<span className="label">{this.props.children}</span>
|
||||
];
|
||||
}
|
||||
}
|
69
js/lib/components/Checkbox.js
Normal file
69
js/lib/components/Checkbox.js
Normal file
@ -0,0 +1,69 @@
|
||||
import Component from 'flarum/Component';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `Checkbox` component defines a checkbox input.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `state` Whether or not the checkbox is checked.
|
||||
* - `className` The class name for the root element.
|
||||
* - `disabled` Whether or not the checkbox is disabled.
|
||||
* - `onchange` A callback to run when the checkbox is checked/unchecked.
|
||||
* - `children` A text label to display next to the checkbox.
|
||||
*/
|
||||
export default class Checkbox extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* Whether or not the checkbox's value is in the process of being saved.
|
||||
*
|
||||
* @type {Boolean}
|
||||
* @public
|
||||
*/
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
view() {
|
||||
let className = 'checkbox ' + (this.props.state ? 'on' : 'off') + ' ' + (this.props.className || '');
|
||||
if (this.loading) className += ' loading';
|
||||
if (this.props.disabled) className += ' disabled';
|
||||
|
||||
return (
|
||||
<label className={className}>
|
||||
<input type="checkbox"
|
||||
checked={this.props.state}
|
||||
disabled={this.props.disabled}
|
||||
onchange={m.withAttr('checked', this.onchange.bind(this))}/>
|
||||
<div className="checkbox-display">
|
||||
{this.getDisplay()}
|
||||
</div>
|
||||
{this.props.children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the checkbox's display (tick/cross icon).
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
getDisplay() {
|
||||
return this.loading
|
||||
? LoadingIndicator.component({size: 'tiny'})
|
||||
: icon(this.props.state ? 'check' : 'times');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback when the state of the checkbox is changed.
|
||||
*
|
||||
* @param {Boolean} checked
|
||||
* @protected
|
||||
*/
|
||||
onchange(checked) {
|
||||
if (this.props.onchange) this.props.onchange(checked, this);
|
||||
}
|
||||
}
|
69
js/lib/components/Dropdown.js
Normal file
69
js/lib/components/Dropdown.js
Normal file
@ -0,0 +1,69 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `Dropdown` component displays a button which, when clicked, shows a
|
||||
* dropdown menu beneath it.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `buttonClassName` A class name to apply to the dropdown toggle button.
|
||||
* - `menuClassName` A class name to apply to the dropdown menu.
|
||||
* - `icon` The name of an icon to show in the dropdown toggle button. Defaults
|
||||
* to 'ellipsis-v'.
|
||||
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||
*
|
||||
* The children will be displayed as a list inside of the dropdown menu.
|
||||
*/
|
||||
export default class Dropdown extends Component {
|
||||
static initProps(props) {
|
||||
props.className = props.className || '';
|
||||
props.buttonClassName = props.buttonClassName || '';
|
||||
props.contentClassName = props.contentClassName || '';
|
||||
props.icon = props.icon || 'ellipsis-v';
|
||||
props.label = props.label || app.trans('controls');
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className={'dropdown btn-group ' + this.props.className}>
|
||||
{this.getButton()}
|
||||
<ul className={'dropdown-menu ' + this.props.menuClassName}>
|
||||
{listItems(this.props.children)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
getButton() {
|
||||
return (
|
||||
<a href="javascript:;"
|
||||
className={'dropdown-toggle ' + this.props.buttonClassName}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.props.onclick}>
|
||||
{this.getButtonContent()}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button's content.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
getButtonContent() {
|
||||
return [
|
||||
icon(this.props.icon),
|
||||
<span className="label">{this.props.label}</span>,
|
||||
icon('caret-down', {className: 'caret'})
|
||||
];
|
||||
}
|
||||
}
|
22
js/lib/components/FieldSet.js
Normal file
22
js/lib/components/FieldSet.js
Normal file
@ -0,0 +1,22 @@
|
||||
import Component from 'flarum/Component';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `FieldSet` component defines a collection of fields, displayed in a list
|
||||
* underneath a title. Accepted properties are:
|
||||
*
|
||||
* - `className` The class name for the fieldset.
|
||||
* - `label` The title of this group of fields.
|
||||
*
|
||||
* The children should be an array of items to show in the fieldset.
|
||||
*/
|
||||
export default class FieldSet extends Component {
|
||||
view() {
|
||||
return (
|
||||
<fieldset className={this.props.className}>
|
||||
<legend>{this.props.label}</legend>
|
||||
<ul>{listItems(this.props.children)}</ul>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
}
|
32
js/lib/components/LinkButton.js
Normal file
32
js/lib/components/LinkButton.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `LinkButton` component defines a `Button` which links to a route.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* All of the props accepted by `Button`, plus:
|
||||
*
|
||||
* - `active` Whether or not the page that this button links to is currently
|
||||
* active.
|
||||
* - `href` The URL to link to. If the current URL `m.route()` matches this,
|
||||
* the `active` prop will automatically be set to true.
|
||||
*/
|
||||
export default class LinkButton extends Button {
|
||||
static initProps(props) {
|
||||
props.active = this.isActive(props);
|
||||
props.config = props.config || m.route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a component with the given props is 'active'.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Boolean}
|
||||
*/
|
||||
static isActive(props) {
|
||||
return typeof props.active !== 'undefined'
|
||||
? props.active
|
||||
: m.route() === props.href;
|
||||
}
|
||||
}
|
27
js/lib/components/LoadingIndicator.js
Normal file
27
js/lib/components/LoadingIndicator.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `LoadingIndicator` component displays a loading spinner with spin.js. It
|
||||
* may have the following special props:
|
||||
*
|
||||
* - `size` The spin.js size preset to use. Defaults to 'small'.
|
||||
*
|
||||
* All other props will be assigned as attributes on the element.
|
||||
*/
|
||||
export default class LoadingIndicator extends Component {
|
||||
view() {
|
||||
const attrs = Object.assign({}, this.props);
|
||||
|
||||
attrs.className = 'loading-indicator ' + (attrs.className || '');
|
||||
delete attrs.size;
|
||||
|
||||
return <div {...attrs}>{m.trust(' ')}</div>;
|
||||
}
|
||||
|
||||
config() {
|
||||
const size = this.props.size || 'small';
|
||||
|
||||
$.fn.spin.presets[size].zIndex = 'auto';
|
||||
this.$().spin(size);
|
||||
}
|
||||
}
|
82
js/lib/components/ModalManager.js
Normal file
82
js/lib/components/ModalManager.js
Normal file
@ -0,0 +1,82 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Modal from 'flarum/components/Modal';
|
||||
|
||||
/**
|
||||
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
||||
* can be shown at once; loading a new component into the ModalManager will
|
||||
* overwrite the previous one.
|
||||
*/
|
||||
export default class ModalManager extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="modal">
|
||||
{this.component && this.component.render()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
this.$()
|
||||
.on('hidden.bs.modal', this.clear.bind(this))
|
||||
.on('shown.bs.modal', this.onready.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a modal dialog.
|
||||
*
|
||||
* @param {Modal} component
|
||||
* @public
|
||||
*/
|
||||
show(component) {
|
||||
if (!(component instanceof Modal)) {
|
||||
throw new Error('The ModalManager component can only show Modal components');
|
||||
}
|
||||
|
||||
clearTimeout(this.hideTimeout);
|
||||
|
||||
this.component = component;
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
this.$().modal('show');
|
||||
this.onready();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal dialog.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
close() {
|
||||
// Don't hide the modal immediately, because if the consumer happens to call
|
||||
// the `show` method straight after to show another modal dialog, it will
|
||||
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
|
||||
// bit to give the `show` method the opportunity to prevent this from going
|
||||
// ahead.
|
||||
this.hideTimeout = setTimeout(() => this.$().modal('hide'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear content from the modal area.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
clear() {
|
||||
this.component = null;
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the modal dialog is ready to be used, tell it!
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
onready() {
|
||||
if (this.component && this.component.onready) {
|
||||
this.component.onready(this.$());
|
||||
}
|
||||
}
|
||||
}
|
96
js/lib/components/Navigation.js
Normal file
96
js/lib/components/Navigation.js
Normal file
@ -0,0 +1,96 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Button from 'flarum/components/Button';
|
||||
|
||||
/**
|
||||
* The `Navigation` component displays a set of navigation buttons. Typically
|
||||
* this is just a back button which pops the app's History. If the user is on
|
||||
* the root page and there is no history to pop, then in some instances it may
|
||||
* show a button that toggles the app's drawer.
|
||||
*
|
||||
* If the app has a pane, it will also include a 'pin' button which toggles the
|
||||
* pinned state of the pane.
|
||||
*
|
||||
* Accepts the following props:
|
||||
*
|
||||
* - `className` The name of a class to set on the root element.
|
||||
* - `drawer` Whether or not to show a button to toggle the app's drawer if
|
||||
* there is no more history to pop.
|
||||
*/
|
||||
export default class Navigation extends Component {
|
||||
view() {
|
||||
const {history, pane} = app;
|
||||
|
||||
return (
|
||||
<div className={'navigation ' + (this.props.className || '')}
|
||||
onmouseenter={pane && pane.show.bind(pane)}
|
||||
onmouseleave={pane && pane.onmouseleave.bind(pane)}>
|
||||
<div className="btn-group">
|
||||
{history.canGoBack()
|
||||
? [this.getBackButton(), this.getPaneButton()]
|
||||
: this.getDrawerButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
// Since this component is 'above' the content of the page (that is, it is a
|
||||
// part of the global UI that persists between routes), we will flag the DOM
|
||||
// to be retained across route changes.
|
||||
context.retain = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the back button.
|
||||
*
|
||||
* @return {Object}
|
||||
* @protected
|
||||
*/
|
||||
getBackButton() {
|
||||
const {history} = app;
|
||||
|
||||
return Button.component({
|
||||
className: 'btn btn-default btn-icon navigation-back',
|
||||
onclick: history.back.bind(history),
|
||||
icon: 'chevron-left'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pane pinned toggle button.
|
||||
*
|
||||
* @return {Object|String}
|
||||
* @protected
|
||||
*/
|
||||
getPaneButton() {
|
||||
const {pane} = app;
|
||||
|
||||
if (!pane || !pane.active) return '';
|
||||
|
||||
return Button.component({
|
||||
className: 'btn btn-default btn-icon navigation-pin' + (pane.pinned ? ' active' : ''),
|
||||
onclick: pane.togglePinned.bind(pane),
|
||||
icon: 'thumb-tack'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the drawer toggle button.
|
||||
*
|
||||
* @return {Object|String}
|
||||
* @protected
|
||||
*/
|
||||
getDrawerButton() {
|
||||
if (!this.props.drawer) return '';
|
||||
|
||||
const {drawer} = app;
|
||||
const user = app.session.user;
|
||||
|
||||
return Button.component({
|
||||
className: 'btn btn-default btn-icon navigation-drawer' +
|
||||
(user && user.unreadNotificationsCount() ? ' unread' : ''),
|
||||
onclick: drawer.toggle.bind(drawer),
|
||||
icon: 'reorder'
|
||||
});
|
||||
}
|
||||
}
|
25
js/lib/components/Select.js
Normal file
25
js/lib/components/Select.js
Normal file
@ -0,0 +1,25 @@
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `Select` component displays a <select> input, surrounded with some extra
|
||||
* elements for styling. It accepts the following props:
|
||||
*
|
||||
* - `options` A map of option values to labels.
|
||||
* - `onchange` A callback to run when the selected value is changed.
|
||||
* - `value` The value of the selected option.
|
||||
*/
|
||||
export default class Select extends Component {
|
||||
view() {
|
||||
const {options, onchange, value} = this.props;
|
||||
|
||||
return (
|
||||
<span className="select">
|
||||
<select className="form-control" onchange={m.withAttr('value', onchange.bind(this))} value={value}>
|
||||
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
|
||||
</select>
|
||||
{icon('sort', {className: 'caret'})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
25
js/lib/components/SelectDropdown.js
Normal file
25
js/lib/components/SelectDropdown.js
Normal file
@ -0,0 +1,25 @@
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
||||
* button's label is set as the label of the first child which has a truthy
|
||||
* `active` prop.
|
||||
*/
|
||||
export default class SelectDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className += ' select-dropdown';
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const activeChild = this.props.children.filter(child => child.props.active)[0];
|
||||
const label = activeChild && activeChild.props.label;
|
||||
|
||||
return [
|
||||
<span className="label">{label}</span>,
|
||||
icon('sort', {className: 'caret'})
|
||||
];
|
||||
}
|
||||
}
|
50
js/lib/components/SplitDropdown.js
Normal file
50
js/lib/components/SplitDropdown.js
Normal file
@ -0,0 +1,50 @@
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import Button from 'flarum/components/Button';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
|
||||
/**
|
||||
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
|
||||
* is displayed as its own button prior to the toggle button.
|
||||
*/
|
||||
export default class SplitDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className += ' split-dropdown';
|
||||
props.menuClassName += ' dropdown-menu-right';
|
||||
}
|
||||
|
||||
getButton() {
|
||||
// Make a copy of the props of the first child component. We will assign
|
||||
// these props to a new button, so that it has exactly the same behaviour as
|
||||
// the first child.
|
||||
const firstChild = this.getFirstChild();
|
||||
const buttonProps = Object.assign({}, firstChild.props);
|
||||
buttonProps.className = (buttonProps.className || '') + ' ' + this.props.buttonClassName;
|
||||
|
||||
return [
|
||||
Button.component(buttonProps),
|
||||
<a href="javascript:;"
|
||||
className={'dropdown-toggle btn-icon ' + this.props.buttonClassName}
|
||||
data-toggle="dropdown">
|
||||
{icon(this.props.icon)}
|
||||
{icon('caret-down', {className: 'caret'})}
|
||||
</a>
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child. If the first child is an array, the first item in that
|
||||
* array will be returned.
|
||||
*
|
||||
* @return {*}
|
||||
* @protected
|
||||
*/
|
||||
getFirstChild() {
|
||||
let firstChild = this.props.children;
|
||||
|
||||
while (firstChild instanceof Array) firstChild = firstChild[0];
|
||||
|
||||
return firstChild;
|
||||
}
|
||||
}
|
17
js/lib/components/Switch.js
Normal file
17
js/lib/components/Switch.js
Normal file
@ -0,0 +1,17 @@
|
||||
import Checkbox from 'flarum/components/Checkbox';
|
||||
|
||||
/**
|
||||
* The `Switch` component is a `Checkbox`, but with a switch display instead of
|
||||
* a tick/cross one.
|
||||
*/
|
||||
export default class Switch extends Checkbox {
|
||||
static initProps(props) {
|
||||
super.initProps(props);
|
||||
|
||||
props.className += ' switch';
|
||||
}
|
||||
|
||||
getDisplay() {
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
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;
|
||||
|
||||
if (attrs.disabled) {
|
||||
attrs.className = (attrs.className || '')+' disabled';
|
||||
delete attrs.onclick;
|
||||
delete attrs.disabled;
|
||||
}
|
||||
|
||||
attrs.href = attrs.href || 'javascript:;';
|
||||
return m('a'+(iconName ? '.has-icon' : ''), attrs, [
|
||||
iconName ? icon(iconName+' icon') : '', ' ',
|
||||
m('span.label', label)
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,33 +1,56 @@
|
||||
import Component from 'flarum/component';
|
||||
import ActionButton from 'flarum/components/action-button';
|
||||
import listItems from 'flarum/helpers/list-items';
|
||||
import Component from 'flarum/Component';
|
||||
import Button from 'flarum/components/Button';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
/**
|
||||
* The `Alert` component represents an alert box, which contains a message,
|
||||
* some controls, and may be dismissible.
|
||||
*
|
||||
* The alert may have the following special props:
|
||||
*
|
||||
* - `type` The type of alert this is. Will be used to give the alert a class
|
||||
* name of `alert-{type}`.
|
||||
* - `controls` An array of controls to show in the alert.
|
||||
* - `dismissible` Whether or not the alert can be dismissed.
|
||||
* - `ondismiss` A callback to run when the alert is dismissed.
|
||||
*
|
||||
* All other props will be assigned as attributes on the alert element.
|
||||
*/
|
||||
export default class Alert extends Component {
|
||||
view() {
|
||||
var attrs = {};
|
||||
for (var i in this.props) { attrs[i] = this.props[i]; }
|
||||
const attrs = Object.assign({}, this.props);
|
||||
|
||||
attrs.className = (attrs.className || '') + ' alert-'+attrs.type;
|
||||
delete attrs.type;
|
||||
const type = extract(attrs, 'type');
|
||||
attrs.className = 'alert alert-' + type + ' ' + (attrs.className || '');
|
||||
|
||||
var message = attrs.message;
|
||||
delete attrs.message;
|
||||
const children = extract(attrs, 'children');
|
||||
const controls = extract(attrs, 'controls') || [];
|
||||
|
||||
var controlItems = attrs.controls ? attrs.controls.slice() : [];
|
||||
delete attrs.controls;
|
||||
// If the alert is meant to be dismissible (which is the case by default),
|
||||
// then we will create a dismiss button to append as the final control in
|
||||
// the alert.
|
||||
const dismissible = extract(attrs, 'dismissible');
|
||||
const ondismiss = extract(attrs, 'ondismiss');
|
||||
const dismissControl = [];
|
||||
|
||||
if (attrs.dismissible || attrs.dismissible === undefined) {
|
||||
controlItems.push(ActionButton.component({
|
||||
if (dismissible || dismissible === undefined) {
|
||||
dismissControl.push(Button.component({
|
||||
icon: 'times',
|
||||
className: 'btn btn-icon btn-link',
|
||||
onclick: attrs.ondismiss.bind(this)
|
||||
className: 'btn btn-link btn-icon dismiss',
|
||||
onclick: ondismiss
|
||||
}));
|
||||
}
|
||||
delete attrs.dismissible;
|
||||
|
||||
return m('div.alert', attrs, [
|
||||
m('span.alert-text', message),
|
||||
m('ul.alert-controls', listItems(controlItems))
|
||||
]);
|
||||
return (
|
||||
<div {...attrs}>
|
||||
<span className="alert-body">
|
||||
{children}
|
||||
</span>
|
||||
<ul className="alert-controls">
|
||||
{listItems(controls.concat(dismissControl))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,68 @@
|
||||
import Component from 'flarum/component';
|
||||
import Component from 'flarum/Component';
|
||||
import Alert from 'flarum/components/Alert';
|
||||
|
||||
/**
|
||||
* The `Alerts` component provides an area in which `Alert` components can be
|
||||
* shown and dismissed.
|
||||
*/
|
||||
export default class Alerts extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
/**
|
||||
* An array of Alert components which are currently showing.
|
||||
*
|
||||
* @type {Alert[]}
|
||||
* @protected
|
||||
*/
|
||||
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);
|
||||
}));
|
||||
return (
|
||||
<div className="alerts">
|
||||
{this.components.map(component => <div className="alerts-item">{component}</div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an Alert in the alerts area.
|
||||
*
|
||||
* @param {Alert} component
|
||||
* @public
|
||||
*/
|
||||
show(component) {
|
||||
if (!(component instanceof Alert)) {
|
||||
throw new Error('The Alerts component can only show Alert components');
|
||||
}
|
||||
|
||||
component.props.ondismiss = this.dismiss.bind(this, component);
|
||||
|
||||
this.components.push(component);
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an alert.
|
||||
*
|
||||
* @param {Alert} component
|
||||
* @public
|
||||
*/
|
||||
dismiss(component) {
|
||||
var index = this.components.indexOf(component);
|
||||
const index = this.components.indexOf(component);
|
||||
|
||||
if (index !== -1) {
|
||||
this.components.splice(index, 1);
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all alerts.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
clear() {
|
||||
this.components = [];
|
||||
m.redraw();
|
||||
|
@ -1,31 +0,0 @@
|
||||
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')),
|
||||
pane && pane.active ? m('button.btn.btn-default.btn-icon.pin'+(pane.pinned ? '.active' : ''), {onclick: pane.togglePinned.bind(pane)}, icon('thumb-tack icon')) : '',
|
||||
]) : (this.props.drawer ? [
|
||||
m('button.btn.btn-default.btn-icon.drawer-toggle', {
|
||||
onclick: app.drawer.toggle.bind(app.drawer),
|
||||
className: app.session.user() && app.session.user().unreadNotificationsCount() ? 'unread-notifications' : ''
|
||||
}, icon('reorder icon'))
|
||||
] : ''));
|
||||
}
|
||||
|
||||
onload(element, isInitialized, context) {
|
||||
context.retain = true;
|
||||
}
|
||||
}
|
@ -1,21 +1,42 @@
|
||||
import Component from 'flarum/component';
|
||||
import Component from 'flarum/Component';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import extract from 'flarum/utils/extract';
|
||||
|
||||
/**
|
||||
* The `Badge` component represents a user/discussion badge, indicating some
|
||||
* status (e.g. a discussion is stickied, a user is an admin).
|
||||
*
|
||||
* A badge may have the following special props:
|
||||
*
|
||||
* - `type` The type of badge this is. This will be used to give the badge a
|
||||
* class name of `badge-{type}`.
|
||||
* - `icon` The name of an icon to show inside the badge.
|
||||
*
|
||||
* All other props will be assigned as attributes on the badge element.
|
||||
*/
|
||||
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, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).tooltip();
|
||||
};
|
||||
this.props.className = 'badge '+(this.props.className || '');
|
||||
this.props.key = this.props.className;
|
||||
view() {
|
||||
const attrs = Object.assign({}, this.props);
|
||||
const type = extract(attrs, 'type');
|
||||
const iconName = extract(attrs, 'icon');
|
||||
|
||||
return m('span', this.props, [
|
||||
icon(iconName+' icon-glyph'),
|
||||
m('span.label', label)
|
||||
]);
|
||||
attrs.className = 'badge badge-' + type + ' ' + (attrs.className || '');
|
||||
|
||||
// Give the badge a unique key so that when badges are displayed together,
|
||||
// and then one is added/removed, Mithril will correctly redraw the series
|
||||
// of badges.
|
||||
attrs.key = attrs.className;
|
||||
|
||||
return (
|
||||
<span {...attrs}>
|
||||
{iconName ? icon(iconName, {className: 'icon'}) : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
this.$().tooltip();
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
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 icon'),
|
||||
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))
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
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))
|
||||
])
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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;
|
||||
while (firstItem instanceof Array) {
|
||||
firstItem = firstItem[0];
|
||||
}
|
||||
|
||||
var items = listItems(this.props.items);
|
||||
|
||||
var buttonProps = {};
|
||||
for (var i in firstItem.props) {
|
||||
buttonProps[i] = firstItem.props[i];
|
||||
}
|
||||
buttonProps.className = (buttonProps.className || '')+' '+(this.props.buttonClass || 'btn btn-default');
|
||||
|
||||
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 btn-icon '+this.props.buttonClass, 'data-toggle': 'dropdown'}, [
|
||||
icon('caret-down icon-caret'),
|
||||
icon((this.props.icon || 'ellipsis-v')+' icon'),
|
||||
]),
|
||||
m('ul', {className: 'dropdown-menu '+(this.props.menuClass || 'pull-right')}, items)
|
||||
])
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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))
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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(' '));
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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.render())
|
||||
}
|
||||
|
||||
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) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.component = component;
|
||||
m.redraw(true);
|
||||
this.$().modal('show');
|
||||
this.ready();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.hideTimeout = setTimeout(() => this.$().modal('hide'));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.component = null;
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.component && this.component.ready && this.component.ready(this.$());
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import Component from 'flarum/component'
|
||||
import icon from 'flarum/helpers/icon'
|
||||
|
||||
export default class NavItem extends Component {
|
||||
view() {
|
||||
var active = this.constructor.active(this.props);
|
||||
return m('li'+(active ? '.active' : ''), m('a.has-icon', {
|
||||
href: this.props.href,
|
||||
onclick: this.props.onclick,
|
||||
config: m.route
|
||||
}, [
|
||||
icon(this.props.icon+' icon'),
|
||||
this.props.label, ' ',
|
||||
this.props.badge ? m('span.count', this.props.badge) : ''
|
||||
]))
|
||||
}
|
||||
|
||||
static active(props) {
|
||||
return typeof props.active !== 'undefined' ? props.active : m.route() === props.href;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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}, [
|
||||
Object.keys(this.props.options).map(key => m('option', {value: key}, this.props.options[key]))
|
||||
]),
|
||||
icon('sort')
|
||||
])
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
import Component from 'flarum/component';
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
|
||||
* The `Separator` component defines a menu separator item.
|
||||
*/
|
||||
class Separator extends Component {
|
||||
view() {
|
||||
return m('span');
|
||||
return <li className="divider"/>;
|
||||
}
|
||||
}
|
||||
|
||||
Separator.wrapperClass = 'divider';
|
||||
Separator.isListItem = true;
|
||||
|
||||
export default Separator;
|
||||
|
@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
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', [
|
||||
m('textarea.form-control.flexible-height', {
|
||||
config: this.configTextarea.bind(this),
|
||||
oninput: m.withAttr('value', this.oninput.bind(this)),
|
||||
placeholder: this.props.placeholder || '',
|
||||
disabled: !!this.props.disabled,
|
||||
value: this.value()
|
||||
}),
|
||||
m('ul.text-editor-controls', 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',
|
||||
onclick: this.onsubmit.bind(this)
|
||||
})
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
this.value(content);
|
||||
this.$('textarea').val(content).trigger('input');
|
||||
}
|
||||
|
||||
setSelectionRange(start, end) {
|
||||
var $textarea = this.$('textarea');
|
||||
$textarea[0].setSelectionRange(start, end);
|
||||
$textarea.focus();
|
||||
}
|
||||
|
||||
getSelectionRange() {
|
||||
var $textarea = this.$('textarea');
|
||||
return [$textarea[0].selectionStart, $textarea[0].selectionEnd];
|
||||
}
|
||||
|
||||
insertAtCursor(insert) {
|
||||
var textarea = this.$('textarea')[0];
|
||||
var content = this.value();
|
||||
var index = textarea ? textarea.selectionStart : content.length;
|
||||
this.setContent(content.slice(0, index)+insert+content.slice(index));
|
||||
if (textarea) {
|
||||
var pos = index + insert.length;
|
||||
this.setSelectionRange(pos, pos);
|
||||
}
|
||||
}
|
||||
|
||||
oninput(value) {
|
||||
this.value(value);
|
||||
this.props.onchange(this.value());
|
||||
|
||||
m.redraw.strategy('none');
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
this.props.onsubmit(this.value());
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user