1
0
mirror of https://github.com/flarum/core.git synced 2025-10-12 07:24:27 +02:00
* Replace gulp with webpack and npm scripts for JS compilation
* Set up Travis CI to commit compiled JS
* Restructure `js` directory; only one instance of npm, forum/admin are "submodules"
* Refactor JS initializers into Application subclasses
* Maintain partial compatibility API (importing from absolute paths) for extensions
* Remove minification responsibility from PHP asset compiler
* Restructure `less` directory
This commit is contained in:
Toby Zerner
2018-06-20 13:20:31 +09:30
committed by GitHub
parent d234badbb2
commit 3f683dd6ee
235 changed files with 9351 additions and 57639 deletions

View File

@@ -0,0 +1,57 @@
import Component from '../Component';
import Button from './Button';
import listItems from '../helpers/listItems';
import extract from '../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() {
const attrs = Object.assign({}, this.props);
const type = extract(attrs, 'type');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const children = extract(attrs, 'children');
const controls = extract(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 (dismissible || dismissible === undefined) {
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>
</div>
);
}
}

View File

@@ -0,0 +1,75 @@
import Component from '../Component';
import Alert from './Alert';
/**
* The `AlertManager` component provides an area in which `Alert` components can
* be shown and dismissed.
*/
export default class AlertManager extends Component {
init() {
/**
* An array of Alert components which are currently showing.
*
* @type {Alert[]}
* @protected
*/
this.components = [];
}
view() {
return (
<div className="AlertManager">
{this.components.map(component => <div className="AlertManager-alert">{component}</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;
}
/**
* Show an Alert in the alerts area.
*
* @param {Alert} component
* @public
*/
show(component) {
if (!(component instanceof Alert)) {
throw new Error('The AlertManager 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) {
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();
}
}

View File

@@ -0,0 +1,39 @@
import Component from '../Component';
import icon from '../helpers/icon';
import extract from '../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.
* - `label`
*
* All other props will be assigned as attributes on the badge element.
*/
export default class Badge extends Component {
view() {
const attrs = Object.assign({}, this.props);
const type = extract(attrs, 'type');
const iconName = extract(attrs, 'icon');
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>
);
}
config(isInitialized) {
if (isInitialized) return;
if (this.props.label) this.$().tooltip({container: 'body'});
}
}

View File

@@ -0,0 +1,64 @@
import Component from '../Component';
import icon from '../helpers/icon';
import extract from '../utils/extract';
import extractText from '../utils/extractText';
import LoadingIndicator from './LoadingIndicator';
/**
* 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.
* - `loading` Whether or not the button should be in a disabled loading state.
*
* 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.type = attrs.type || 'button';
// If nothing else is provided, we use the textual button content as tooltip
if (!attrs.title && this.props.children) {
attrs.title = extractText(this.props.children);
}
const iconName = extract(attrs, 'icon');
if (iconName) attrs.className += ' hasIcon';
const loading = extract(attrs, 'loading');
if (attrs.disabled || loading) {
attrs.className += ' disabled' + (loading ? ' loading' : '');
delete attrs.onclick;
}
return <button {...attrs}>{this.getButtonContent()}</button>;
}
/**
* Get the template for the button's content.
*
* @return {*}
* @protected
*/
getButtonContent() {
const iconName = this.props.icon;
return [
iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '',
this.props.children ? <span className="Button-label">{this.props.children}</span> : '',
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
];
}
}

View File

@@ -0,0 +1,67 @@
import Component from '../Component';
import LoadingIndicator from './LoadingIndicator';
import icon from '../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 {
init() {
/**
* 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 ? 'fas fa-check' : 'fas fa-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);
}
}

View File

@@ -0,0 +1,131 @@
import Component from '../Component';
import icon from '../helpers/icon';
import listItems from '../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.
* - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
* - `onhide`
* - `onshow`
*
* The children will be displayed as a list inside of the dropdown menu.
*/
export default class Dropdown extends Component {
static initProps(props) {
super.initProps(props);
props.className = props.className || '';
props.buttonClassName = props.buttonClassName || '';
props.menuClassName = props.menuClassName || '';
props.label = props.label || '';
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-caret-down';
}
init() {
this.showing = false;
}
view() {
const items = this.props.children ? listItems(this.props.children) : [];
return (
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton()}
{this.getMenu(items)}
</div>
);
}
config(isInitialized) {
if (isInitialized) return;
// When opening the dropdown menu, work out if the menu goes beyond the
// bottom of the viewport. If it does, we will apply class to make it show
// above the toggle button instead of below it.
this.$().on('shown.bs.dropdown', () => {
this.showing = true;
if (this.props.onshow) {
this.props.onshow();
}
m.redraw();
const $menu = this.$('.Dropdown-menu');
const isRight = $menu.hasClass('Dropdown-menu--right');
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$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()
);
});
this.$().on('hidden.bs.dropdown', () => {
this.showing = false;
if (this.props.onhide) {
this.props.onhide();
}
m.redraw();
});
}
/**
* Get the template for the button.
*
* @return {*}
* @protected
*/
getButton() {
return (
<button
className={'Dropdown-toggle ' + this.props.buttonClassName}
data-toggle="dropdown"
onclick={this.props.onclick}>
{this.getButtonContent()}
</button>
);
}
/**
* Get the template for the button's content.
*
* @return {*}
* @protected
*/
getButtonContent() {
return [
this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '',
<span className="Button-label">{this.props.label}</span>,
this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
];
}
getMenu(items) {
return (
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
{items}
</ul>
);
}
}

View File

@@ -0,0 +1,22 @@
import Component from '../Component';
import listItems from '../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>
);
}
}

View File

@@ -0,0 +1,16 @@
import Badge from './Badge';
export default class GroupBadge extends Badge {
static initProps(props) {
super.initProps(props);
if (props.group) {
props.icon = props.group.icon();
props.style = {backgroundColor: props.group.color()};
props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
props.type = 'group--' + props.group.id();
delete props.group;
}
}
}

View File

@@ -0,0 +1,40 @@
import Button from './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;
}
view() {
const vdom = super.view();
vdom.tag = 'a';
return vdom;
}
/**
* 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;
}
}

View File

@@ -0,0 +1,36 @@
import Component from '../Component';
import { Spinner } from 'spin.js';
/**
* 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 = 'LoadingIndicator ' + (attrs.className || '');
delete attrs.size;
return <div {...attrs}>{m.trust('&nbsp;')}</div>;
}
config() {
const options = { zIndex: 'auto', color: this.$().css('color') };
switch (this.props.size) {
case 'large':
Object.assign(options, { lines: 10, length: 8, width: 4, radius: 8 });
break;
default:
Object.assign(options, { lines: 8, length: 4, width: 3, radius: 5 });
}
new Spinner(options).spin(this.element);
}
}

View File

@@ -0,0 +1,139 @@
import Component from '../Component';
import Alert from './Alert';
import Button from './Button';
/**
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
* should implement the `className`, `title`, and `content` methods.
*
* @abstract
*/
export default class Modal extends Component {
init() {
/**
* An alert component to show below the header.
*
* @type {Alert}
*/
this.alert = null;
}
view() {
if (this.alert) {
this.alert.props.dismissible = false;
}
return (
<div className={'Modal modal-dialog ' + this.className()}>
<div className="Modal-content">
{this.isDismissible() ? (
<div className="Modal-close App-backControl">
{Button.component({
icon: 'fas fa-times',
onclick: this.hide.bind(this),
className: 'Button Button--icon Button--link'
})}
</div>
) : ''}
<form onsubmit={this.onsubmit.bind(this)}>
<div className="Modal-header">
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
</div>
{alert ? <div className="Modal-alert">{this.alert}</div> : ''}
{this.content()}
</form>
</div>
</div>
);
}
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*
* @return {Boolean}
*/
isDismissible() {
return true;
}
/**
* Get the class name to apply to the modal.
*
* @return {String}
* @abstract
*/
className() {
}
/**
* Get the title of the modal dialog.
*
* @return {String}
* @abstract
*/
title() {
}
/**
* Get the content of the modal.
*
* @return {VirtualElement}
* @abstract
*/
content() {
}
/**
* Handle the modal form's submit event.
*
* @param {Event} e
*/
onsubmit() {
}
/**
* Focus on the first input when the modal is ready to be used.
*/
onready() {
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {
}
/**
* Hide the modal.
*/
hide() {
app.modal.close();
}
/**
* Stop loading.
*/
loaded() {
this.loading = false;
m.redraw();
}
/**
* Show an alert describing an error returned from the API, and give focus to
* the first relevant field.
*
* @param {RequestError} error
*/
onerror(error) {
this.alert = error.alert;
m.redraw();
if (error.status === 422 && error.response.errors) {
this.$('form [name=' + error.response.errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
} else {
this.onready();
}
}
}

View File

@@ -0,0 +1,106 @@
import Component from '../Component';
import Modal from './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 {
init() {
this.showing = false;
this.component = null;
}
view() {
return (
<div className="ModalManager modal fade">
{this.component && this.component.render()}
</div>
);
}
config(isInitialized, context) {
if (isInitialized) return;
// 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;
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.showing = true;
this.component = component;
app.current.retain = true;
m.redraw(true);
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
this.onready();
}
/**
* Close the modal dialog.
*
* @public
*/
close() {
if (!this.showing) return;
// 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');
this.showing = false;
});
}
/**
* Clear content from the modal area.
*
* @protected
*/
clear() {
if (this.component) {
this.component.onhide();
}
this.component = null;
app.current.retain = false;
m.lazyRedraw();
}
/**
* When the modal dialog is ready to be used, tell it!
*
* @protected
*/
onready() {
if (this.component && this.component.onready) {
this.component.onready(this.$());
}
}
}

View File

@@ -0,0 +1,106 @@
import Component from '../Component';
import Button from './Button';
import LinkButton from './LinkButton';
/**
* 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 ButtonGroup ' + (this.props.className || '')}
onmouseenter={pane && pane.show.bind(pane)}
onmouseleave={pane && pane.onmouseleave.bind(pane)}>
{history.canGoBack()
? [this.getBackButton(), this.getPaneButton()]
: this.getDrawerButton()}
</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;
const previous = history.getPrevious() || {};
return LinkButton.component({
className: 'Button Navigation-back Button--icon',
href: history.backUrl(),
icon: 'fas fa-chevron-left',
title: previous.title,
config: () => {},
onclick: e => {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
e.preventDefault();
history.back();
}
});
}
/**
* Get the pane pinned toggle button.
*
* @return {Object|String}
* @protected
*/
getPaneButton() {
const {pane} = app;
if (!pane || !pane.active) return '';
return Button.component({
className: 'Button Button--icon Navigation-pin' + (pane.pinned ? ' active' : ''),
onclick: pane.togglePinned.bind(pane),
icon: 'fas fa-thumbtack'
});
}
/**
* 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: 'Button Button--icon Navigation-drawer' +
(user && user.newNotificationsCount() ? ' new' : ''),
onclick: e => {
e.stopPropagation();
drawer.show();
},
icon: 'fas fa-bars'
});
}
}

View File

@@ -0,0 +1,19 @@
import Component from '../Component';
/**
* The `Placeholder` component displays a muted text with some call to action,
* usually used as an empty state.
*
* ### Props
*
* - `text`
*/
export default class Placeholder extends Component {
view() {
return (
<div className="Placeholder">
<p>{this.props.text}</p>
</div>
);
}
}

View File

@@ -0,0 +1,30 @@
import Modal from './Modal';
export default class RequestErrorModal extends Modal {
className() {
return 'RequestErrorModal Modal--large';
}
title() {
return this.props.error.xhr
? this.props.error.xhr.status+' '+this.props.error.xhr.statusText
: '';
}
content() {
let responseText;
try {
responseText = JSON.stringify(JSON.parse(this.props.error.responseText), null, 2);
} catch (e) {
responseText = this.props.error.responseText;
}
return <div className="Modal-body">
<pre>
{this.props.error.options.method} {this.props.error.options.url}<br/><br/>
{responseText}
</pre>
</div>;
}
}

View File

@@ -0,0 +1,25 @@
import Component from '../Component';
import icon from '../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="Select-input FormControl" onchange={onchange ? m.withAttr('value', onchange.bind(this)) : undefined} value={value}>
{Object.keys(options).map(key => <option value={key}>{options[key]}</option>)}
</select>
{icon('fas fa-sort', {className: 'Select-caret'})}
</span>
);
}
}

View File

@@ -0,0 +1,34 @@
import Dropdown from './Dropdown';
import icon from '../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.
*
* ### Props
*
* - `caretIcon`
* - `defaultLabel`
*/
export default class SelectDropdown extends Dropdown {
static initProps(props) {
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'fas fa-sort';
super.initProps(props);
props.className += ' Dropdown--select';
}
getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0];
let label = activeChild && activeChild.props.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'})
];
}
}

View File

@@ -0,0 +1,14 @@
import Component from '../Component';
/**
* The `Separator` component defines a menu separator item.
*/
class Separator extends Component {
view() {
return <li className="Dropdown-separator"/>;
}
}
Separator.isListItem = true;
export default Separator;

View File

@@ -0,0 +1,50 @@
import Dropdown from './Dropdown';
import Button from './Button';
import icon from '../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 += ' Dropdown--split';
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 || '') + ' SplitDropdown-button Button ' + this.props.buttonClassName;
return [
Button.component(buttonProps),
<button
className={'Dropdown-toggle Button Button--icon ' + this.props.buttonClassName}
data-toggle="dropdown">
{icon(this.props.icon, {className: 'Button-icon'})}
{icon('fas fa-caret-down', {className: 'Button-caret'})}
</button>
];
}
/**
* 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;
}
}

View File

@@ -0,0 +1,17 @@
import Checkbox from './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 = (props.className || '') + ' Checkbox--switch';
}
getDisplay() {
return this.loading ? super.getDisplay() : '';
}
}