import React, { Component } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { HEXToINTColor, INTToHEXColor } from '/imports/utils/hexInt'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import RenderInBrowser from 'react-render-in-browser'; import browser from 'browser-detect'; import { noop } from 'lodash'; import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component'; import { styles } from './styles.scss'; import ToolbarMenuItem from './toolbar-menu-item/component'; import ToolbarSubmenu from './toolbar-submenu/component'; 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 intlMessages = defineMessages({ toolbarTools: { id: 'app.whiteboard.toolbar.tools', description: 'Whiteboard toolbar tools menu', }, toolbarLineThickness: { id: 'app.whiteboard.toolbar.thickness', description: 'Whiteboard toolbar thickness menu', }, toolbarLineThicknessDisabled: { id: 'app.whiteboard.toolbar.thicknessDisabled', description: 'Whiteboard toolbar thickness menu', }, toolbarLineColor: { id: 'app.whiteboard.toolbar.color', description: 'Whiteboard toolbar colors menu', }, toolbarLineColorDisabled: { id: 'app.whiteboard.toolbar.colorDisabled', 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', }, }); const runExceptInEdge = fn => (browser().name === 'edge' ? noop : fn); class WhiteboardToolbar extends Component { constructor(props) { super(props); const { annotations } = this.props; const isMobile = browser().mobile; let annotationSelected = { icon: isMobile ? 'hand' : 'pen_tool', value: isMobile ? 'hand' : 'pencil', }; if (!annotations.some(el => el.value === annotationSelected.value) && annotations.length > 0) { annotationSelected = annotations[annotations.length - 1]; } this.state = { // a variable to control which list is currently open currentSubmenuOpen: '', // variables to keep current selected draw settings annotationSelected, thicknessSelected: { value: 4 }, colorSelected: { value: '#ff0000' }, fontSizeSelected: { value: 20 }, // keeping the previous color and the thickness icon's radius selected for svg animation prevColorSelected: { value: '#ff0000' }, 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 onBlurEnabled: true, }; this.displaySubMenu = this.displaySubMenu.bind(this); this.closeSubMenu = this.closeSubMenu.bind(this); this.handleUndo = this.handleUndo.bind(this); this.handleClearAll = this.handleClearAll.bind(this); this.handleSwitchWhiteboardMode = this.handleSwitchWhiteboardMode.bind(this); this.handleAnnotationChange = this.handleAnnotationChange.bind(this); this.handleThicknessChange = this.handleThicknessChange.bind(this); this.handleFontSizeChange = this.handleFontSizeChange.bind(this); this.handleColorChange = this.handleColorChange.bind(this); this.handleMouseEnter = this.handleMouseEnter.bind(this); this.handleMouseLeave = this.handleMouseLeave.bind(this); this.componentDidMount = runExceptInEdge(this.componentDidMount); this.componentDidUpdate = runExceptInEdge(this.componentDidUpdate); } 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( annotationSelected.value, thicknessSelected.value * 2, HEXToINTColor(colorSelected.value), fontSizeSelected.value, { textShapeValue: '', textShapeActiveId: '', }, ); } } componentDidMount() { 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(); } } componentDidUpdate(prevProps, prevState) { const { annotations } = this.props; const { annotationSelected } = prevState; const hadInAnnotations = annotations.some(el => el.value === annotationSelected.value); // if color or thickness were changed // we might need to trigger svg animation for Color and Thickness icons this.animateSvgIcons(prevState); if (prevProps.annotations.length !== annotations.length && annotations.length === 0) { this.handleAnnotationChange({ icon: null, value: null }); } if (!hadInAnnotations && annotations.length) { this.handleAnnotationChange(annotations[annotations.length - 1]); } } setToolbarValues(drawSettings) { // divide by 2, since we need the radius for the thickness icon 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) { 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, 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 if (this.state.colorSelected.value !== prevState.colorSelected.value) { // 1st case b) if (this.state.annotationSelected.value !== 'text') { this.thicknessListIconColor.beginElement(); } // 1st case a) this.colorListIconColor.beginElement(); // 2nd case } else if (this.state.thicknessSelected.value !== prevState.thicknessSelected.value) { this.thicknessListIconRadius.beginElement(); // 3rd case } 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 displaySubMenu(listName) { this.setState({ currentSubmenuOpen: this.state.currentSubmenuOpen === listName ? '' : listName, }); } // close a current submenu (fires onBlur only, when you click anywhere on the screen) closeSubMenu() { // a separate case for the active text shape if (this.state.annotationSelected.value === 'text' && this.props.textShapeActiveId !== '') { return; } if (this.state.onBlurEnabled) { this.setState({ currentSubmenuOpen: undefined, }); } } // undo annotation handleUndo() { this.props.actions.undoAnnotation(this.props.whiteboardId); } // clear all annotations handleClearAll() { this.props.actions.clearWhiteboard(this.props.whiteboardId); } handleSwitchWhiteboardMode() { const { multiUser, whiteboardId } = this.props; this.props.actions.changeWhiteboardMode(!multiUser, whiteboardId); } // changes a current selected annotation both in the state and in the session // and closes the annotation list handleAnnotationChange(annotation) { const obj = { annotationSelected: annotation, onBlurEnabled: true, currentSubmenuOpen: '', }; // to animate thickness icon properly when you switch the tool back from Text if (annotation.value === 'text') { obj.prevThicknessSelected = { value: 0 }; } this.props.actions.setTool(annotation.value); this.setState(obj); } // changes a current selected thickness both in the state and in the session // and closes the thickness list handleThicknessChange(thicknessSelected) { // thickness value * 2 since this is radius, we need to double it this.props.actions.setThickness(thicknessSelected.value * 2); this.setState({ prevThicknessSelected: this.state.thicknessSelected, thicknessSelected, onBlurEnabled: true, currentSubmenuOpen: '', }); } handleFontSizeChange(fontSize) { this.props.actions.setFontSize(fontSize.value); this.setState({ fontSizeSelected: fontSize, onBlurEnabled: true, currentSubmenuOpen: '', }); } // changes a current selected color both in the state and in the session // and closes the color list handleColorChange(color) { this.props.actions.setColor(HEXToINTColor(color.value)); this.setState({ prevColorSelected: this.state.colorSelected, colorSelected: color, onBlurEnabled: true, currentSubmenuOpen: '', }); } // disabling onBlur flag when mouse is over the items in the lists handleMouseEnter() { this.setState({ onBlurEnabled: false, }); } // enabling the onBlur flag when the mouse leaving the lists handleMouseLeave() { this.setState({ onBlurEnabled: true, }); } renderToolItem() { const { intl, annotations } = this.props; const isDisabled = !annotations.length; return ( {this.state.currentSubmenuOpen === 'annotationList' && annotations.length > 1 ? ( ) : null} ); } renderFontItem() { const { intl } = this.props; return ( {this.state.currentSubmenuOpen === 'fontSizeList' ? ( ) : null} ); } renderFontItemIcon() { return (

Aa

); } renderThicknessItem() { const { intl, annotations } = this.props; const isDisabled = this.state.annotationSelected.value === 'hand' || !annotations.length; return ( {this.state.currentSubmenuOpen === 'thicknessList' ? ( ) : null} ); } renderThicknessItemIcon() { return ( { 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" /> { 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" /> ); } renderColorItem() { const { intl, annotations } = this.props; const isDisabled = this.state.annotationSelected.value === 'hand' || !annotations.length; return ( {this.state.currentSubmenuOpen === 'colorList' ? ( ) : null} ); } renderColorItemIcon() { return ( { 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" /> ); } renderUndoItem() { const { intl } = this.props; return ( ); } renderClearAllItem() { const { intl } = this.props; return ( ); } renderMultiUserItem() { const { intl, multiUser } = this.props; return ( ); } render() { const { annotationSelected } = this.state; const { isPresenter } = this.props; return (
{this.renderToolItem()} {annotationSelected.value === 'text' ? this.renderFontItem() : this.renderThicknessItem()} {this.renderColorItem()} {this.renderUndoItem()} {this.renderClearAllItem()} {isPresenter ? this.renderMultiUserItem() : null}
); } } WhiteboardToolbar.defaultProps = { colors: ANNOTATION_COLORS, thicknessRadiuses: THICKNESS_RADIUSES, fontSizes: FONT_SIZES, intl: intlShape, }; 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 fontSizes: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.number.isRequired, }).isRequired), // defines an array of colors for the toolbar (color submenu) colors: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string.isRequired, }).isRequired), // defines an array of thickness values for the toolbar and their corresponding session values thicknessRadiuses: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.number.isRequired, }).isRequired), intl: intlShape, }; export default injectWbResizeEvent(injectIntl(WhiteboardToolbar));