bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/component.jsx

598 lines
19 KiB
React
Raw Normal View History

2017-06-08 05:25:47 +08:00
import React, { Component } from 'react';
import PropTypes from 'prop-types';
2017-04-19 08:54:51 +08:00
import cx from 'classnames';
import { HEXToINTColor, INTToHEXColor } from '/imports/utils/hexInt';
2018-01-11 03:22:42 +08:00
import { defineMessages, injectIntl, intlShape } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
2018-01-08 14:17:18 +08:00
import { styles } from './styles.scss';
2017-09-23 14:27:55 +08:00
import ToolbarMenuItem from './toolbar-menu-item/component';
import ToolbarSubmenu from './toolbar-submenu/component';
2017-02-23 08:10:30 +08:00
2017-09-21 05:05:17 +08:00
const TRANSITION_DURATION = '0.4s';
const TOOLBAR_CONFIG = Meteor.settings.public.whiteboard.toolbar;
const ANNOTATION_COLORS = TOOLBAR_CONFIG.colors;
const THICKNESS_RADIUSES = TOOLBAR_CONFIG.thickness;
const FONT_SIZES = TOOLBAR_CONFIG.font_sizes;
const ANNOTATION_TOOLS = TOOLBAR_CONFIG.tools;
2017-09-21 05:05:17 +08:00
2017-12-08 21:28:02 +08:00
const intlMessages = defineMessages({
toolbarTools: {
id: 'app.whiteboard.toolbar.tools',
description: 'Whiteboard toolbar tools menu',
},
toolbarLineThickness: {
id: 'app.whiteboard.toolbar.thickness',
description: 'Whiteboard toolbar thickness menu',
},
toolbarLineColor: {
id: 'app.whiteboard.toolbar.color',
description: 'Whiteboard toolbar colors menu',
},
toolbarUndoAnnotation: {
id: 'app.whiteboard.toolbar.undo',
description: 'Whiteboard toolbar tools menu',
},
toolbarClearAnnotations: {
id: 'app.whiteboard.toolbar.clear',
description: 'Whiteboard toolbar clear menu',
},
toolbarMultiUserOn: {
id: 'app.whiteboard.toolbar.multiUserOn',
description: 'Whiteboard toolbar turn multi-user on menu',
},
toolbarMultiUserOff: {
id: 'app.whiteboard.toolbar.multiUserOff',
description: 'Whiteboard toolbar turn multi-user off menu',
},
toolbarFontSize: {
id: 'app.whiteboard.toolbar.fontSize',
description: 'Whiteboard toolbar font size menu',
},
});
class WhiteboardToolbar extends Component {
constructor() {
super();
2017-04-19 08:54:51 +08:00
this.state = {
// a variable to control which list is currently open
2017-04-19 08:54:51 +08:00
currentSubmenuOpen: '',
// variables to keep current selected draw settings
annotationSelected: {
icon: 'pen_tool',
value: 'pencil',
},
2017-09-23 14:27:55 +08:00
thicknessSelected: { value: 4 },
colorSelected: { value: '#000000' },
fontSizeSelected: { value: 20 },
// keeping the previous color and the thickness icon's radius selected for svg animation
2017-09-23 14:27:55 +08:00
prevColorSelected: { value: '#000000' },
prevThicknessSelected: { value: 4 },
// lists of tools/thickness/colors are not direct children of main toolbar buttons
// and we want the list to close when onBlur fires at the main toolbar button
// (click anywhere on the screen) thus we have to control the blur manually by disabling it
// when you hover over the buttons in the list and enabling when the mouse leaves the list
2017-04-19 08:54:51 +08:00
onBlurEnabled: true,
};
this.displaySubMenu = this.displaySubMenu.bind(this);
this.closeSubMenu = this.closeSubMenu.bind(this);
this.handleUndo = this.handleUndo.bind(this);
2017-04-22 02:01:52 +08:00
this.handleClearAll = this.handleClearAll.bind(this);
this.handleSwitchWhiteboardMode = this.handleSwitchWhiteboardMode.bind(this);
2017-04-19 08:54:51 +08:00
this.handleAnnotationChange = this.handleAnnotationChange.bind(this);
this.handleThicknessChange = this.handleThicknessChange.bind(this);
this.handleFontSizeChange = this.handleFontSizeChange.bind(this);
2017-04-19 08:54:51 +08:00
this.handleColorChange = this.handleColorChange.bind(this);
2017-09-21 05:05:17 +08:00
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
2017-04-19 08:54:51 +08:00
}
componentWillMount() {
const drawSettings = this.props.actions.getCurrentDrawSettings();
// if there are saved drawSettings in the session storage
// - retrieve them and update toolbar values
if (drawSettings) {
this.setToolbarValues(drawSettings);
// no drawSettings in the sessionStorage - setting default values
} else {
// setting default drawing settings if they haven't been set previously
const { annotationSelected, thicknessSelected, colorSelected, fontSizeSelected } = this.state;
this.props.actions.setInitialWhiteboardToolbarValues(
2017-09-23 14:27:55 +08:00
annotationSelected.value,
thicknessSelected.value * 2,
HEXToINTColor(colorSelected.value),
fontSizeSelected.value,
{
textShapeValue: '',
textShapeActiveId: '',
},
);
}
}
2017-04-19 08:54:51 +08:00
componentDidMount() {
2017-09-23 14:27:55 +08:00
if (this.state.annotationSelected.value !== 'text') {
// trigger initial animation on the thickness circle, otherwise it stays at 0
this.thicknessListIconColor.beginElement();
this.thicknessListIconRadius.beginElement();
this.colorListIconColor.beginElement();
} else {
this.colorListIconColor.beginElement();
}
2017-04-19 08:54:51 +08:00
}
componentDidUpdate(prevProps, prevState) {
// if color or thickness were changed
// we might need to trigger svg animation for Color and Thickness icons
this.animateSvgIcons(prevState);
}
setToolbarValues(drawSettings) {
// divide by 2, since we need the radius for the thickness icon
2017-09-23 14:27:55 +08:00
const thicknessSelected = { value: drawSettings.whiteboardAnnotationThickness / 2 };
const fontSizeSelected = { value: drawSettings.textFontSize };
const colorSelected = { value: INTToHEXColor(drawSettings.whiteboardAnnotationColor) };
let annotationSelected = {};
for (let i = 0; i < this.props.annotations.length; i += 1) {
2017-09-23 14:27:55 +08:00
if (drawSettings.whiteboardAnnotationTool === this.props.annotations[i].value) {
annotationSelected = this.props.annotations[i];
break;
}
}
this.setState({
colorSelected,
fontSizeSelected,
thicknessSelected,
annotationSelected,
});
}
animateSvgIcons(prevState) {
/* Animation for the svg icons that we use for thickness (circle) and color (rectangle)
* has to be triggered manually
* we have 4 main cases:
* 1. Color change -
a) Text tool is selected, Font-Size icon substitutes the thickness icon,
2017-09-06 09:36:15 +08:00
thus we need to trigger the color change just for the color icon
b) Any other tool than Text tool is selected - trigger color change for both icons
* 2. Thickness change - trigger radius for the thickness icon
* 3. Switch from the Text tool to any other - trigger color and radius for thickness
* 4. Trigger initial animation for the icons
*/
// 1st case
2017-09-23 14:27:55 +08:00
if (this.state.colorSelected.value !== prevState.colorSelected.value) {
// 1st case b)
2017-09-23 14:27:55 +08:00
if (this.state.annotationSelected.value !== 'text') {
this.thicknessListIconColor.beginElement();
}
2017-09-06 09:36:15 +08:00
// 1st case a)
this.colorListIconColor.beginElement();
// 2nd case
2017-09-23 14:27:55 +08:00
} else if (this.state.thicknessSelected.value !== prevState.thicknessSelected.value) {
this.thicknessListIconRadius.beginElement();
// 3rd case
2017-09-23 14:27:55 +08:00
} else if (this.state.annotationSelected.value !== 'text' &&
prevState.annotationSelected.value === 'text') {
this.thicknessListIconRadius.beginElement();
this.thicknessListIconColor.beginElement();
}
// 4th case, initial animation is triggered in componentDidMount
}
// open a submenu
2017-04-19 08:54:51 +08:00
displaySubMenu(listName) {
this.setState({
currentSubmenuOpen: this.state.currentSubmenuOpen === listName ? '' : listName,
2017-04-19 08:54:51 +08:00
});
}
// close a current submenu (fires onBlur only, when you click anywhere on the screen)
2017-04-19 08:54:51 +08:00
closeSubMenu() {
// a separate case for the active text shape
2017-09-23 14:27:55 +08:00
if (this.state.annotationSelected.value === 'text' && this.props.textShapeActiveId !== '') {
return;
}
if (this.state.onBlurEnabled) {
2017-04-19 08:54:51 +08:00
this.setState({
currentSubmenuOpen: undefined,
});
}
}
// undo annotation
2017-04-19 08:54:51 +08:00
handleUndo() {
this.props.actions.undoAnnotation(this.props.whiteboardId);
2017-04-19 08:54:51 +08:00
}
// clear all annotations
2017-04-22 02:01:52 +08:00
handleClearAll() {
this.props.actions.clearWhiteboard(this.props.whiteboardId);
2017-04-19 08:54:51 +08:00
}
handleSwitchWhiteboardMode() {
this.props.actions.changeWhiteboardMode(!this.props.multiUser);
}
// changes a current selected annotation both in the state and in the session
// and closes the annotation list
handleAnnotationChange(annotation) {
const obj = {
2017-04-19 08:54:51 +08:00
annotationSelected: annotation,
onBlurEnabled: true,
currentSubmenuOpen: '',
};
// to animate thickness icon properly when you switch the tool back from Text
2017-09-23 14:27:55 +08:00
if (annotation.value === 'text') {
obj.prevThicknessSelected = { value: 0 };
}
2017-09-23 14:27:55 +08:00
this.props.actions.setTool(annotation.value);
this.setState(obj);
2017-04-19 08:54:51 +08:00
}
// changes a current selected thickness both in the state and in the session
// and closes the thickness list
2017-09-23 14:27:55 +08:00
handleThicknessChange(thicknessSelected) {
// thickness value * 2 since this is radius, we need to double it
this.props.actions.setThickness(thicknessSelected.value * 2);
2017-04-19 08:54:51 +08:00
this.setState({
2017-09-23 14:27:55 +08:00
prevThicknessSelected: this.state.thicknessSelected,
thicknessSelected,
2017-04-19 08:54:51 +08:00
onBlurEnabled: true,
currentSubmenuOpen: '',
});
}
2017-09-23 14:27:55 +08:00
handleFontSizeChange(fontSize) {
this.props.actions.setFontSize(fontSize.value);
this.setState({
2017-09-23 14:27:55 +08:00
fontSizeSelected: fontSize,
onBlurEnabled: true,
currentSubmenuOpen: '',
});
}
// changes a current selected color both in the state and in the session
// and closes the color list
2017-04-19 08:54:51 +08:00
handleColorChange(color) {
2017-09-23 14:27:55 +08:00
this.props.actions.setColor(HEXToINTColor(color.value));
2017-04-19 08:54:51 +08:00
this.setState({
prevColorSelected: this.state.colorSelected,
2017-04-19 08:54:51 +08:00
colorSelected: color,
onBlurEnabled: true,
currentSubmenuOpen: '',
});
}
// disabling onBlur flag when mouse is over the items in the lists
2017-09-21 05:05:17 +08:00
handleMouseEnter() {
2017-04-19 08:54:51 +08:00
this.setState({
onBlurEnabled: false,
});
}
// enabling the onBlur flag when the mouse leaving the lists
2017-09-21 05:05:17 +08:00
handleMouseLeave() {
2017-04-19 08:54:51 +08:00
this.setState({
onBlurEnabled: true,
});
}
2017-09-23 14:27:55 +08:00
renderToolItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
2017-09-23 14:27:55 +08:00
return (
<ToolbarMenuItem
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarTools)}
2017-09-23 14:27:55 +08:00
icon={this.state.annotationSelected.icon}
onItemClick={this.displaySubMenu}
objectToReturn={'annotationList'}
onBlur={this.closeSubMenu}
className={cx(styles.toolbarButton, this.state.currentSubmenuOpen === 'annotationList' ? '' : styles.notActive)}
>
{this.state.currentSubmenuOpen === 'annotationList' ?
<ToolbarSubmenu
type="annotations"
customIcon={false}
label="Annotations"
onItemClick={this.handleAnnotationChange}
objectsToRender={this.props.annotations}
objectSelected={this.state.annotationSelected}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
</ToolbarMenuItem>
);
}
2017-04-19 08:54:51 +08:00
2017-09-23 14:27:55 +08:00
renderFontItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
2017-04-19 08:54:51 +08:00
return (
2017-09-23 14:27:55 +08:00
<ToolbarMenuItem
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarFontSize)}
2017-09-23 14:27:55 +08:00
customIcon={this.renderFontItemIcon()}
onItemClick={this.displaySubMenu}
objectToReturn={'fontSizeList'}
onBlur={this.closeSubMenu}
className={cx(styles.toolbarButton, this.state.currentSubmenuOpen === 'fontSizeList' ? '' : styles.notActive)}
>
{this.state.currentSubmenuOpen === 'fontSizeList' ?
<ToolbarSubmenu
type="font-size"
customIcon
label="Font Size"
onItemClick={this.handleFontSizeChange}
objectsToRender={this.props.fontSizes}
objectSelected={this.state.fontSizeSelected}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
</ToolbarMenuItem>
2017-04-19 08:54:51 +08:00
);
}
2017-09-23 14:27:55 +08:00
renderFontItemIcon() {
return (
<p
className={styles.textThickness}
style={{
fontSize: this.state.fontSizeSelected.value,
color: this.state.colorSelected.value,
WebkitTransition: `color ${TRANSITION_DURATION}, font-size ${TRANSITION_DURATION}`, /* Safari */
transition: `color ${TRANSITION_DURATION}, font-size ${TRANSITION_DURATION}`,
}}
>
Aa
</p>
);
}
2017-09-23 14:27:55 +08:00
renderThicknessItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
return (
2017-09-23 14:27:55 +08:00
<ToolbarMenuItem
disabled={this.state.annotationSelected.value === 'hand'}
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarLineThickness)}
2017-09-23 14:27:55 +08:00
onItemClick={this.displaySubMenu}
objectToReturn={'thicknessList'}
onBlur={this.closeSubMenu}
className={cx(styles.toolbarButton, this.state.currentSubmenuOpen === 'thicknessList' ? '' : styles.notActive)}
customIcon={this.renderThicknessItemIcon()}
>
{this.state.currentSubmenuOpen === 'thicknessList' ?
<ToolbarSubmenu
type="thickness"
customIcon
label="Thickness"
onItemClick={this.handleThicknessChange}
objectsToRender={this.props.thicknessRadiuses}
objectSelected={this.state.thicknessSelected}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
</ToolbarMenuItem>
);
}
2017-09-23 14:27:55 +08:00
renderThicknessItemIcon() {
return (
<svg className={styles.customSvgIcon} shapeRendering="geometricPrecision">
<circle
shapeRendering="geometricPrecision"
cx="50%"
cy="50%"
stroke="black"
strokeWidth="1"
>
<animate
ref={(ref) => { this.thicknessListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin={'indefinite'}
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
<animate
ref={(ref) => { this.thicknessListIconRadius = ref; }}
attributeName="r"
attributeType="XML"
from={this.state.prevThicknessSelected.value}
to={this.state.thicknessSelected.value}
begin={'indefinite'}
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</circle>
</svg>
);
}
2017-04-19 08:54:51 +08:00
2017-09-23 14:27:55 +08:00
renderColorItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
2017-04-19 08:54:51 +08:00
return (
2017-09-23 14:27:55 +08:00
<ToolbarMenuItem
disabled={this.state.annotationSelected.value === 'hand'}
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarLineColor)}
2017-09-23 14:27:55 +08:00
onItemClick={this.displaySubMenu}
objectToReturn={'colorList'}
onBlur={this.closeSubMenu}
className={cx(styles.toolbarButton, this.state.currentSubmenuOpen === 'colorList' ? '' : styles.notActive)}
customIcon={this.renderColorItemIcon()}
>
{this.state.currentSubmenuOpen === 'colorList' ?
<ToolbarSubmenu
type="color"
customIcon
label="Color"
onItemClick={this.handleColorChange}
objectsToRender={this.props.colors}
objectSelected={this.state.colorSelected}
handleMouseEnter={this.handleMouseEnter}
handleMouseLeave={this.handleMouseLeave}
/>
: null }
</ToolbarMenuItem>
2017-04-19 08:54:51 +08:00
);
}
2017-09-23 14:27:55 +08:00
renderColorItemIcon() {
return (
<svg className={styles.customSvgIcon}>
<rect x="25%" y="25%" width="50%" height="50%" stroke="black" strokeWidth="1">
<animate
ref={(ref) => { this.colorListIconColor = ref; }}
attributeName="fill"
attributeType="XML"
from={this.state.prevColorSelected.value}
to={this.state.colorSelected.value}
begin={'indefinite'}
dur={TRANSITION_DURATION}
repeatCount="0"
fill="freeze"
/>
</rect>
</svg>
);
}
2017-04-19 08:54:51 +08:00
2017-09-23 14:27:55 +08:00
renderUndoItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
2017-04-19 08:54:51 +08:00
return (
2017-09-23 14:27:55 +08:00
<ToolbarMenuItem
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarUndoAnnotation)}
2017-09-23 14:27:55 +08:00
icon={'undo'}
onItemClick={this.handleUndo}
className={cx(styles.toolbarButton, styles.notActive)}
/>
);
}
renderClearAllItem() {
2017-12-08 21:28:02 +08:00
const { intl } = this.props;
2017-09-23 14:27:55 +08:00
return (
<ToolbarMenuItem
2017-12-08 21:28:02 +08:00
label={intl.formatMessage(intlMessages.toolbarClearAnnotations)}
2017-09-23 14:27:55 +08:00
icon={'circle_close'}
onItemClick={this.handleClearAll}
className={cx(styles.toolbarButton, styles.notActive)}
/>
);
}
renderMultiUserItem() {
2017-12-08 21:28:02 +08:00
const { intl, multiUser } = this.props;
2017-09-23 14:27:55 +08:00
return (
<ToolbarMenuItem
2017-12-08 21:28:02 +08:00
label={multiUser ? intl.formatMessage(intlMessages.toolbarMultiUserOff) : intl.formatMessage(intlMessages.toolbarMultiUserOn)}
2017-09-23 14:27:55 +08:00
icon={multiUser ? 'multi_whiteboard' : 'whiteboard'}
onItemClick={this.handleSwitchWhiteboardMode}
className={cx(styles.toolbarButton, styles.notActive)}
/>
2017-04-19 08:54:51 +08:00
);
}
render() {
2017-09-23 14:27:55 +08:00
const { annotationSelected } = this.state;
const { isPresenter } = this.props;
2017-04-19 08:54:51 +08:00
return (
<div className={styles.toolbarContainer} style={{ height: this.props.height }}>
2017-04-19 08:54:51 +08:00
<div className={styles.toolbarWrapper}>
2017-09-23 14:27:55 +08:00
{this.renderToolItem()}
{annotationSelected.value === 'text' ?
this.renderFontItem()
:
2017-09-23 14:27:55 +08:00
this.renderThicknessItem()
}
{this.renderColorItem()}
{this.renderUndoItem()}
{this.renderClearAllItem()}
{isPresenter ?
this.renderMultiUserItem()
: null }
2017-04-19 08:54:51 +08:00
</div>
</div>
);
}
}
WhiteboardToolbar.defaultProps = {
colors: ANNOTATION_COLORS,
thicknessRadiuses: THICKNESS_RADIUSES,
fontSizes: FONT_SIZES,
annotations: ANNOTATION_TOOLS,
2017-04-19 08:54:51 +08:00
};
WhiteboardToolbar.propTypes = {
// defines a current mode of the whiteboard, multi/single user
multiUser: PropTypes.bool.isRequired,
// defines whether a current user is a presenter or not
isPresenter: PropTypes.bool.isRequired,
// defines an object of available actions
actions: PropTypes.objectOf(PropTypes.func).isRequired,
// defines the id of the active text shape (if any)
// for the separate onBlur case in the closeSubMenu()
textShapeActiveId: PropTypes.string.isRequired,
// defines a current whiteboard id
whiteboardId: PropTypes.string.isRequired,
// defines an array of icons for the toolbar as well as their corresponding session values
annotations: PropTypes.arrayOf(PropTypes.object).isRequired,
// defines an array of font-sizes for the Font-size submenu of the text shape
2017-09-23 14:27:55 +08:00
fontSizes: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number.isRequired,
}).isRequired,
).isRequired,
// defines an array of colors for the toolbar (color submenu)
2017-09-23 14:27:55 +08:00
colors: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
}).isRequired,
).isRequired,
// defines an array of thickness values for the toolbar and their corresponding session values
2017-09-23 14:27:55 +08:00
thicknessRadiuses: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number.isRequired,
}).isRequired,
).isRequired,
// defines the physical height of the whiteboard
height: PropTypes.number.isRequired,
2017-12-08 21:28:02 +08:00
2018-01-11 03:22:42 +08:00
intl: intlShape.isRequired,
2017-12-08 21:28:02 +08:00
};
2017-12-08 21:28:02 +08:00
export default injectWbResizeEvent(injectIntl(WhiteboardToolbar));