ref(toolbar): Implement stateless toolbar
This commit is contained in:
parent
53f675fbe0
commit
b81dc4e59b
|
@ -0,0 +1,65 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a toolbar in React/Web. It is a strip that contains a set of
|
||||||
|
* toolbar items such as buttons. Toolbar is commonly placed inside of a
|
||||||
|
* Toolbox.
|
||||||
|
*
|
||||||
|
* @class Toolbar
|
||||||
|
* @extends Component
|
||||||
|
*/
|
||||||
|
export default class StatelessToolbar extends Component {
|
||||||
|
/**
|
||||||
|
* Base toolbar component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
/**
|
||||||
|
* Children of current React component.
|
||||||
|
*/
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbar's class name.
|
||||||
|
*/
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for mouse out event.
|
||||||
|
*/
|
||||||
|
onMouseOut: React.PropTypes.func,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for mouse over event.
|
||||||
|
*/
|
||||||
|
onMouseOver: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render(): ReactElement<*> {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
onMouseOut,
|
||||||
|
onMouseOver
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className = { `toolbar ${className}` }
|
||||||
|
onMouseOut = { onMouseOut }
|
||||||
|
onMouseOver = { onMouseOver }>
|
||||||
|
{
|
||||||
|
this.props.children
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AbstractToolbarButton from './AbstractToolbarButton';
|
||||||
|
|
||||||
|
type MapOfAttributes = { [key: string]: * };
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable flowtype/space-before-type-colon */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes toolbar button props and maps them to HTML attributes to set.
|
||||||
|
*
|
||||||
|
* @param {Object} props - Props set to the React component.
|
||||||
|
* @returns {MapOfAttributes}
|
||||||
|
*/
|
||||||
|
function getButtonAttributesByProps(props: Object = {})
|
||||||
|
: MapOfAttributes {
|
||||||
|
// XXX Make sure to not modify props.classNames because that'd be bad
|
||||||
|
// practice.
|
||||||
|
const classNames = (props.classNames && [ ...props.classNames ]) || [];
|
||||||
|
|
||||||
|
props.toggled && classNames.push('toggled');
|
||||||
|
props.unclickable && classNames.push('unclickable');
|
||||||
|
|
||||||
|
const result: MapOfAttributes = {
|
||||||
|
className: classNames.join(' '),
|
||||||
|
'data-container': 'body',
|
||||||
|
'data-placement': 'bottom',
|
||||||
|
id: props.id
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!props.enabled) {
|
||||||
|
result.disabled = 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.hidden) {
|
||||||
|
result.style = { display: 'none' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tooltipText) {
|
||||||
|
result.content = props.tooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-enable flowtype/space-before-type-colon */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a button in Toolbar on React.
|
||||||
|
*
|
||||||
|
* @class ToolbarButton
|
||||||
|
* @extends AbstractToolbarButton
|
||||||
|
*/
|
||||||
|
export default class StatelessToolbarButton extends AbstractToolbarButton {
|
||||||
|
_onClick: Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbar button component's property types.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
*/
|
||||||
|
static propTypes = {
|
||||||
|
...AbstractToolbarButton.propTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object describing button.
|
||||||
|
*/
|
||||||
|
button: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for button's reference.
|
||||||
|
*/
|
||||||
|
createRefToButton: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes new ToolbarButton instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only properties with which the new
|
||||||
|
* instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: Object) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// Bind methods to save the context
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render(): ReactElement<*> {
|
||||||
|
const { button } = this.props;
|
||||||
|
const attributes = getButtonAttributesByProps(button);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{ ...attributes }
|
||||||
|
onClick = { this._onClick }
|
||||||
|
ref = { this.props.createRefToButton }>
|
||||||
|
{ this._renderInnerElementsIfRequired() }
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper on on click handler props for current button.
|
||||||
|
*
|
||||||
|
* @param {Event} event - Click event object.
|
||||||
|
* @returns {void}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onClick(event: Event): void {
|
||||||
|
const {
|
||||||
|
button,
|
||||||
|
onClick
|
||||||
|
} = this.props;
|
||||||
|
const {
|
||||||
|
enabled,
|
||||||
|
unclickable
|
||||||
|
} = button;
|
||||||
|
|
||||||
|
if (enabled && !unclickable && onClick) {
|
||||||
|
onClick(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If toolbar button should contain children elements
|
||||||
|
* renders them.
|
||||||
|
*
|
||||||
|
* @returns {ReactElement|null}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderInnerElementsIfRequired(): ReactElement<*> | null {
|
||||||
|
if (this.props.button.html) {
|
||||||
|
return this.props.button.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { setToolbarHovered } from '../actions';
|
import { setToolbarHovered } from '../actions';
|
||||||
|
|
||||||
|
import StatelessToolbar from './StatelessToolbar';
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +17,8 @@ import ToolbarButton from './ToolbarButton';
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class Toolbar extends Component {
|
class Toolbar extends Component {
|
||||||
|
_onMouseOut: Function;
|
||||||
|
_onMouseOver: Function;
|
||||||
_renderToolbarButton: Function;
|
_renderToolbarButton: Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,10 +60,12 @@ class Toolbar extends Component {
|
||||||
*
|
*
|
||||||
* @param {Object} props - Object containing React component properties.
|
* @param {Object} props - Object containing React component properties.
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props: Object) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// Bind callbacks to preverse this.
|
// Bind callbacks to preverse this.
|
||||||
|
this._onMouseOut = this._onMouseOut.bind(this);
|
||||||
|
this._onMouseOver = this._onMouseOver.bind(this);
|
||||||
this._renderToolbarButton = this._renderToolbarButton.bind(this);
|
this._renderToolbarButton = this._renderToolbarButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,21 +76,36 @@ class Toolbar extends Component {
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
render(): ReactElement<*> {
|
render(): ReactElement<*> {
|
||||||
const { className } = this.props;
|
const toolbarButtons = new Map();
|
||||||
|
|
||||||
|
this.props.toolbarButtons
|
||||||
|
.forEach((button, key) => {
|
||||||
|
const { onClick } = button;
|
||||||
|
|
||||||
|
toolbarButtons.set(key, {
|
||||||
|
...button,
|
||||||
|
onClick: (...args) =>
|
||||||
|
onClick && onClick(this.props.dispatch, ...args)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...this.props,
|
||||||
|
onMouseOut: this._onMouseOut,
|
||||||
|
onMouseOver: this._onMouseOver,
|
||||||
|
toolbarButtons
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<StatelessToolbar { ...props }>
|
||||||
className = { `toolbar ${className}` }
|
|
||||||
onMouseOut = { this._onMouseOut }
|
|
||||||
onMouseOver = { this._onMouseOver }>
|
|
||||||
{
|
{
|
||||||
[ ...this.props.toolbarButtons.entries() ]
|
[ ...this.props.toolbarButtons.entries() ]
|
||||||
.reduce(this._renderToolbarButton, [])
|
.map(this._renderToolbarButton)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
this.props.children
|
this.props.children
|
||||||
}
|
}
|
||||||
</div>
|
</StatelessToolbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,47 +130,38 @@ class Toolbar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders toolbar button. Method is passed to reduce function.
|
* Renders toolbar button. Method is passed to map function.
|
||||||
*
|
*
|
||||||
* @param {Array} acc - Toolbar buttons array.
|
|
||||||
* @param {Array} keyValuePair - Key value pair containing button and its
|
* @param {Array} keyValuePair - Key value pair containing button and its
|
||||||
* key.
|
* key.
|
||||||
* @private
|
* @private
|
||||||
* @returns {Array} Array of toolbar buttons.
|
* @returns {ReactElement} A toolbar button.
|
||||||
*/
|
*/
|
||||||
_renderToolbarButton(acc: Array<*>,
|
_renderToolbarButton(
|
||||||
keyValuePair: Array<*>): Array<ReactElement<*>> {
|
keyValuePair: Array<*>): ReactElement<*> {
|
||||||
const [ key, button ] = keyValuePair;
|
const [ key, button ] = keyValuePair;
|
||||||
|
|
||||||
if (button.component) {
|
if (button.component) {
|
||||||
acc.push(
|
return (
|
||||||
<button.component
|
<button.component
|
||||||
key = { key }
|
key = { key }
|
||||||
tooltipPosition = { this.props.tooltipPosition } />
|
tooltipPosition = { this.props.tooltipPosition } />
|
||||||
);
|
);
|
||||||
|
|
||||||
return acc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tooltipPosition } = this.props;
|
const { tooltipPosition } = this.props;
|
||||||
|
|
||||||
const { onClick, onMount, onUnmount } = button;
|
const { onClick, onMount, onUnmount } = button;
|
||||||
|
|
||||||
const onClickHandler
|
return (
|
||||||
= (...args) =>
|
|
||||||
onClick(this.props.dispatch, ...args);
|
|
||||||
|
|
||||||
acc.push(
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
button = { button }
|
button = { button }
|
||||||
key = { key }
|
key = { key }
|
||||||
onClick = { onClickHandler }
|
onClick = { onClick }
|
||||||
onMount = { onMount }
|
onMount = { onMount }
|
||||||
onUnmount = { onUnmount }
|
onUnmount = { onUnmount }
|
||||||
tooltipPosition = { tooltipPosition } />
|
tooltipPosition = { tooltipPosition } />
|
||||||
);
|
);
|
||||||
|
|
||||||
return acc;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
|
|
||||||
|
@ -9,8 +9,7 @@ import {
|
||||||
setTooltipText
|
setTooltipText
|
||||||
} from '../../../../modules/UI/util/Tooltip';
|
} from '../../../../modules/UI/util/Tooltip';
|
||||||
|
|
||||||
import AbstractToolbarButton from './AbstractToolbarButton';
|
import StatelessToolbarButton from './StatelessToolbarButton';
|
||||||
import { getButtonAttributesByProps } from '../functions';
|
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
|
@ -20,18 +19,17 @@ declare var APP: Object;
|
||||||
* @class ToolbarButton
|
* @class ToolbarButton
|
||||||
* @extends AbstractToolbarButton
|
* @extends AbstractToolbarButton
|
||||||
*/
|
*/
|
||||||
class ToolbarButton extends AbstractToolbarButton {
|
class ToolbarButton extends Component {
|
||||||
|
button: Object;
|
||||||
_createRefToButton: Function;
|
_createRefToButton: Function;
|
||||||
|
|
||||||
_onClick: Function;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toolbar button component's property types.
|
* Toolbar button component's property types.
|
||||||
*
|
*
|
||||||
* @static
|
* @static
|
||||||
*/
|
*/
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
...AbstractToolbarButton.propTypes,
|
...StatelessToolbarButton.propTypes,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object describing button.
|
* Object describing button.
|
||||||
|
@ -71,7 +69,6 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
|
|
||||||
// Bind methods to save the context
|
// Bind methods to save the context
|
||||||
this._createRefToButton = this._createRefToButton.bind(this);
|
this._createRefToButton = this._createRefToButton.bind(this);
|
||||||
this._onClick = this._onClick.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -109,17 +106,17 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
*/
|
*/
|
||||||
render(): ReactElement<*> {
|
render(): ReactElement<*> {
|
||||||
const { button } = this.props;
|
const { button } = this.props;
|
||||||
const attributes = getButtonAttributesByProps(button);
|
|
||||||
const popups = button.popups || [];
|
const popups = button.popups || [];
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...this.props,
|
||||||
|
createRefToButton: this._createRefToButton
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<StatelessToolbarButton { ...props }>
|
||||||
{ ...attributes }
|
|
||||||
onClick = { this._onClick }
|
|
||||||
ref = { this._createRefToButton }>
|
|
||||||
{ this._renderInnerElementsIfRequired() }
|
|
||||||
{ this._renderPopups(popups) }
|
{ this._renderPopups(popups) }
|
||||||
</a>
|
</StatelessToolbarButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,28 +132,6 @@ class ToolbarButton extends AbstractToolbarButton {
|
||||||
this.button = element;
|
this.button = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper on on click handler props for current button.
|
|
||||||
*
|
|
||||||
* @param {Event} event - Click event object.
|
|
||||||
* @returns {void}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_onClick(event: Event): void {
|
|
||||||
const {
|
|
||||||
button,
|
|
||||||
onClick
|
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
enabled,
|
|
||||||
unclickable
|
|
||||||
} = button;
|
|
||||||
|
|
||||||
if (enabled && !unclickable && onClick) {
|
|
||||||
onClick(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If toolbar button should contain children elements
|
* If toolbar button should contain children elements
|
||||||
* renders them.
|
* renders them.
|
||||||
|
|
|
@ -3,55 +3,12 @@ import SideContainerToggler
|
||||||
|
|
||||||
import defaultToolbarButtons from './defaultToolbarButtons';
|
import defaultToolbarButtons from './defaultToolbarButtons';
|
||||||
|
|
||||||
type MapOfAttributes = { [key: string]: * };
|
|
||||||
|
|
||||||
declare var $: Function;
|
declare var $: Function;
|
||||||
declare var AJS: Object;
|
declare var AJS: Object;
|
||||||
declare var interfaceConfig: Object;
|
declare var interfaceConfig: Object;
|
||||||
|
|
||||||
export { abstractMapStateToProps } from './functions.native';
|
export { abstractMapStateToProps } from './functions.native';
|
||||||
|
|
||||||
/* eslint-disable flowtype/space-before-type-colon */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes toolbar button props and maps them to HTML attributes to set.
|
|
||||||
*
|
|
||||||
* @param {Object} props - Props set to the React component.
|
|
||||||
* @returns {MapOfAttributes}
|
|
||||||
*/
|
|
||||||
export function getButtonAttributesByProps(props: Object = {})
|
|
||||||
: MapOfAttributes {
|
|
||||||
// XXX Make sure to not modify props.classNames because that'd be bad
|
|
||||||
// practice.
|
|
||||||
const classNames = (props.classNames && [ ...props.classNames ]) || [];
|
|
||||||
|
|
||||||
props.toggled && classNames.push('toggled');
|
|
||||||
props.unclickable && classNames.push('unclickable');
|
|
||||||
|
|
||||||
const result: MapOfAttributes = {
|
|
||||||
className: classNames.join(' '),
|
|
||||||
'data-container': 'body',
|
|
||||||
'data-placement': 'bottom',
|
|
||||||
id: props.id
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!props.enabled) {
|
|
||||||
result.disabled = 'disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.hidden) {
|
|
||||||
result.style = { display: 'none' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.tooltipText) {
|
|
||||||
result.content = props.tooltipText;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable flowtype/space-before-type-colon */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object which contains the default buttons for the primary and
|
* Returns an object which contains the default buttons for the primary and
|
||||||
* secondary toolbars.
|
* secondary toolbars.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './actionTypes';
|
export * from './actionTypes';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
export * from './functions';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
import './reducer';
|
import './reducer';
|
||||||
|
|
Loading…
Reference in New Issue