ref(toolbar): Implement stateless toolbar

This commit is contained in:
hristoterezov 2017-07-31 11:02:41 +03:00 committed by virtuacoplenny
parent 53f675fbe0
commit b81dc4e59b
6 changed files with 262 additions and 104 deletions

View File

@ -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>
);
}
}

View File

@ -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;
}
}

View File

@ -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;
} }
} }

View File

@ -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.

View File

@ -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.

View File

@ -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';