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

552 lines
17 KiB
React
Raw Normal View History

2022-06-01 00:52:37 +08:00
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import deviceInfo from '/imports/utils/deviceInfo';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/common/button/component';
import {
2022-09-01 00:06:33 +08:00
HUNDRED_PERCENT,
MAX_PERCENT,
STEP,
2022-06-01 00:52:37 +08:00
} from '/imports/utils/slideCalcUtils';
import {
PresentationToolbarItemType,
} from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/presentation-toolbar-item/enums';
2022-06-01 00:52:37 +08:00
import Styled from './styles';
import ZoomTool from './zoom-tool/component';
import SmartMediaShareContainer from './smart-video-share/container';
2022-06-01 00:52:37 +08:00
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
import KEY_CODES from '/imports/utils/keyCodes';
import Spinner from '/imports/ui/components/common/spinner/component';
import Separator from '/imports/ui/components/common/separator/component';
const intlMessages = defineMessages({
previousSlideLabel: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.prevSlideLabel',
description: 'Previous slide button label',
},
2019-06-06 23:04:02 +08:00
previousSlideDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.prevSlideDesc',
description: 'Aria description for when switching to previous slide',
2019-06-06 23:04:02 +08:00
},
nextSlideLabel: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.nextSlideLabel',
description: 'Next slide button label',
},
2019-06-06 23:04:02 +08:00
nextSlideDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.nextSlideDesc',
description: 'Aria description for when switching to next slide',
2019-06-06 23:04:02 +08:00
},
noNextSlideDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.noNextSlideDesc',
description: '',
2019-06-06 23:04:02 +08:00
},
noPrevSlideDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.noPrevSlideDesc',
description: '',
2019-06-06 23:04:02 +08:00
},
skipSlideLabel: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.skipSlideLabel',
description: 'Aria label for when switching to a specific slide',
2019-06-06 23:04:02 +08:00
},
skipSlideDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.skipSlideDesc',
description: 'Aria description for when switching to a specific slide',
2019-06-06 23:04:02 +08:00
},
goToSlide: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.goToSlide',
description: 'button for slide select',
},
2019-03-12 00:21:12 +08:00
selectLabel: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.selectLabel',
description: 'slide select label',
2019-03-12 00:21:12 +08:00
},
2018-10-25 01:54:19 +08:00
fitToWidth: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.fitToWidth',
description: 'button for fit to width',
2018-10-24 04:34:09 +08:00
},
2019-06-06 23:04:02 +08:00
fitToWidthDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.fitWidthDesc',
description: 'Aria description to display the whole width of the slide',
2019-06-06 23:04:02 +08:00
},
fitToPage: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.fitToPage',
description: 'button label for fit to width',
},
2019-06-06 23:04:02 +08:00
fitToPageDesc: {
2022-06-01 00:52:37 +08:00
id: 'app.presentation.presentationToolbar.fitScreenDesc',
description: 'Aria description to display the whole slide',
2019-06-06 23:04:02 +08:00
},
presentationLabel: {
2022-06-01 00:52:37 +08:00
id: 'app.presentationUploder.title',
description: 'presentation area element label',
},
toolbarMultiUserOn: {
2022-06-01 00:52:37 +08:00
id: 'app.whiteboard.toolbar.multiUserOn',
description: 'Whiteboard toolbar turn multi-user on menu',
},
toolbarMultiUserOff: {
2022-06-01 00:52:37 +08:00
id: 'app.whiteboard.toolbar.multiUserOff',
description: 'Whiteboard toolbar turn multi-user off menu',
},
2022-08-15 06:49:39 +08:00
pan: {
id: 'app.whiteboard.toolbar.tools.hand',
description: 'presentation toolbar pan label',
},
});
class PresentationToolbar extends PureComponent {
2016-08-03 06:55:20 +08:00
constructor(props) {
super(props);
2023-08-20 08:21:03 +08:00
this.state = {
wasFTWActive: false,
};
this.setWasActive = this.setWasActive.bind(this);
this.handleFTWSlideChange = this.handleFTWSlideChange.bind(this);
this.handleSkipToSlideChange = this.handleSkipToSlideChange.bind(this);
2018-08-23 01:49:33 +08:00
this.change = this.change.bind(this);
2019-06-06 23:04:02 +08:00
this.renderAriaDescs = this.renderAriaDescs.bind(this);
this.nextSlideHandler = this.nextSlideHandler.bind(this);
this.previousSlideHandler = this.previousSlideHandler.bind(this);
this.fullscreenToggleHandler = this.fullscreenToggleHandler.bind(this);
this.switchSlide = this.switchSlide.bind(this);
this.handleSwitchWhiteboardMode = this.handleSwitchWhiteboardMode.bind(this);
}
componentDidMount() {
2022-06-01 00:52:37 +08:00
document.addEventListener('keydown', this.switchSlide);
}
componentDidUpdate(prevProps) {
2023-08-20 08:21:03 +08:00
const { zoom, setIsPanning, fitToWidth, fitToWidthHandler, currentSlideNum } = this.props;
const { wasFTWActive } = this.state;
if (zoom <= HUNDRED_PERCENT && zoom !== prevProps.zoom && !fitToWidth) setIsPanning();
2023-08-20 08:21:03 +08:00
if ((prevProps?.currentSlideNum !== currentSlideNum) && (!fitToWidth && wasFTWActive)) {
setTimeout(() => {
fitToWidthHandler();
this.setWasActive(false);
}, 150)
}
}
componentWillUnmount() {
2022-06-01 00:52:37 +08:00
document.removeEventListener('keydown', this.switchSlide);
}
2023-08-20 08:21:03 +08:00
setWasActive(wasFTWActive) {
this.setState({ wasFTWActive });
}
handleFTWSlideChange() {
const { fitToWidth, fitToWidthHandler } = this.props;
if (fitToWidth) {
fitToWidthHandler();
this.setWasActive(fitToWidth);
}
}
handleSkipToSlideChange(event) {
2024-01-20 22:01:20 +08:00
const { skipToSlide } = this.props;
2018-12-22 00:03:55 +08:00
const requestedSlideNum = Number.parseInt(event.target.value, 10);
2021-12-01 20:56:55 +08:00
2023-08-20 08:21:03 +08:00
this.handleFTWSlideChange();
2021-12-01 20:56:55 +08:00
if (event) event.currentTarget.blur();
2024-01-20 22:01:20 +08:00
skipToSlide(requestedSlideNum);
2016-08-03 06:55:20 +08:00
}
handleSwitchWhiteboardMode() {
const {
multiUser,
whiteboardId,
removeWhiteboardGlobalAccess,
addWhiteboardGlobalAccess,
} = this.props;
if (multiUser) {
return removeWhiteboardGlobalAccess(whiteboardId);
}
return addWhiteboardGlobalAccess(whiteboardId);
2016-08-03 06:55:20 +08:00
}
fullscreenToggleHandler() {
const {
fullscreenElementId,
isFullscreen,
layoutContextDispatch,
fullscreenAction,
fullscreenRef,
handleToggleFullScreen,
} = this.props;
handleToggleFullScreen(fullscreenRef);
2022-06-01 00:52:37 +08:00
const newElement = isFullscreen ? '' : fullscreenElementId;
layoutContextDispatch({
type: fullscreenAction,
value: {
element: newElement,
2022-06-01 00:52:37 +08:00
group: '',
},
});
}
nextSlideHandler(event) {
2024-01-20 22:01:20 +08:00
const { nextSlide, endCurrentPoll } = this.props;
2023-08-20 08:21:03 +08:00
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
2023-03-16 02:16:56 +08:00
endCurrentPoll();
2024-01-20 22:01:20 +08:00
nextSlide();
}
previousSlideHandler(event) {
2024-01-20 22:01:20 +08:00
const { previousSlide, endCurrentPoll } = this.props;
2023-08-20 08:21:03 +08:00
this.handleFTWSlideChange();
if (event) event.currentTarget.blur();
2023-03-16 02:16:56 +08:00
endCurrentPoll();
2024-01-20 22:01:20 +08:00
previousSlide();
}
switchSlide(event) {
const { target, which } = event;
const isBody = target.nodeName === 'BODY';
if (isBody) {
switch (which) {
case KEY_CODES.ARROW_LEFT:
case KEY_CODES.PAGE_UP:
this.previousSlideHandler();
break;
case KEY_CODES.ARROW_RIGHT:
case KEY_CODES.PAGE_DOWN:
this.nextSlideHandler();
break;
case KEY_CODES.ENTER:
this.fullscreenToggleHandler();
break;
default:
}
}
}
2018-08-23 01:49:33 +08:00
change(value) {
2018-12-22 00:03:55 +08:00
const { zoomChanger } = this.props;
zoomChanger(value);
2018-08-23 01:49:33 +08:00
}
renderToolbarPluginItems() {
let pluginProvidedItems = [];
if (this.props) {
const {
pluginProvidedPresentationToolbarItems,
} = this.props;
pluginProvidedItems = pluginProvidedPresentationToolbarItems;
}
return pluginProvidedItems?.map((ppb) => {
let componentToReturn;
const ppbId = ppb.id;
switch (ppb.type) {
case PresentationToolbarItemType.BUTTON:
componentToReturn = (
<Button
key={ppbId}
style={{ marginLeft: '2px' }}
label={ppb.label}
onClick={ppb.onClick}
tooltipLabel={ppb.tooltip}
/>
);
break;
case PresentationToolbarItemType.SPINNER:
componentToReturn = (
<Spinner
key={ppbId}
/>
);
break;
case PresentationToolbarItemType.SEPARATOR:
componentToReturn = (
<Separator />
);
break;
default:
componentToReturn = null;
}
return componentToReturn;
});
}
2019-06-06 23:04:02 +08:00
renderAriaDescs() {
const { intl } = this.props;
return (
<div hidden>
{/* Aria description's for toolbar buttons */}
<div id="prevSlideDesc">
{intl.formatMessage(intlMessages.previousSlideDesc)}
</div>
<div id="noPrevSlideDesc">
{intl.formatMessage(intlMessages.noPrevSlideDesc)}
</div>
<div id="nextSlideDesc">
{intl.formatMessage(intlMessages.nextSlideDesc)}
</div>
<div id="noNextSlideDesc">
{intl.formatMessage(intlMessages.noNextSlideDesc)}
</div>
<div id="skipSlideDesc">
{intl.formatMessage(intlMessages.skipSlideDesc)}
</div>
<div id="fitWidthDesc">
{intl.formatMessage(intlMessages.fitToWidthDesc)}
</div>
<div id="fitPageDesc">
{intl.formatMessage(intlMessages.fitToPageDesc)}
</div>
</div>
);
}
renderSkipSlideOpts(numberOfSlides) {
// Fill drop down menu with all the slides in presentation
const { intl } = this.props;
const optionList = [];
for (let i = 1; i <= numberOfSlides; i += 1) {
optionList.push(
<option value={i} key={i}>
{intl.formatMessage(intlMessages.goToSlide, { 0: i })}
</option>,
);
}
return optionList;
}
2016-08-03 06:55:20 +08:00
render() {
const {
currentSlideNum,
numberOfSlides,
2018-12-22 00:03:55 +08:00
fitToWidthHandler,
fitToWidth,
intl,
2018-08-23 01:49:33 +08:00
zoom,
2019-06-27 00:29:34 +08:00
isMeteorConnected,
isPollingEnabled,
amIPresenter,
startPoll,
currentSlide,
slidePosition,
multiUserSize,
multiUser,
2016-08-03 06:55:20 +08:00
} = this.props;
2021-04-01 01:13:36 +08:00
const { isMobile } = deviceInfo;
2019-03-12 00:21:12 +08:00
const startOfSlides = !(currentSlideNum > 1);
const endOfSlides = !(currentSlideNum < numberOfSlides);
2019-06-06 23:04:02 +08:00
const prevSlideAriaLabel = startOfSlides
? intl.formatMessage(intlMessages.previousSlideLabel)
: `${intl.formatMessage(intlMessages.previousSlideLabel)} (${currentSlideNum <= 1 ? '' : currentSlideNum - 1
})`;
2019-06-06 23:04:02 +08:00
const nextSlideAriaLabel = endOfSlides
? intl.formatMessage(intlMessages.nextSlideLabel)
: `${intl.formatMessage(intlMessages.nextSlideLabel)} (${currentSlideNum >= 1 ? currentSlideNum + 1 : ''
})`;
2016-08-03 06:55:20 +08:00
return (
2021-11-08 21:09:56 +08:00
<Styled.PresentationToolbarWrapper
id="presentationToolbarWrapper"
>
2019-06-06 23:04:02 +08:00
{this.renderAriaDescs()}
2023-07-31 22:24:25 +08:00
<Styled.QuickPollButtonWrapper>
{this.renderToolbarPluginItems()}
{isPollingEnabled ? (
<Styled.QuickPollButton
{...{
intl,
amIPresenter,
startPoll,
currentSlide,
}}
2018-08-23 01:49:33 +08:00
/>
) : null}
<SmartMediaShareContainer {...{ intl, currentSlide }} />
2023-07-31 22:24:25 +08:00
</Styled.QuickPollButtonWrapper>
<Styled.PresentationSlideControls>
<Styled.PrevSlideButton
role="button"
aria-label={prevSlideAriaLabel}
aria-describedby={
startOfSlides ? 'noPrevSlideDesc' : 'prevSlideDesc'
}
disabled={startOfSlides || !isMeteorConnected}
color="light"
circle
icon="left_arrow"
size="md"
onClick={this.previousSlideHandler}
label={intl.formatMessage(intlMessages.previousSlideLabel)}
hideLabel
data-test="prevSlide"
/>
2019-03-12 00:21:12 +08:00
<TooltipContainer
title={intl.formatMessage(intlMessages.selectLabel)}
>
<Styled.SkipSlideSelect
id="skipSlide"
aria-label={intl.formatMessage(intlMessages.skipSlideLabel)}
aria-describedby="skipSlideDesc"
aria-live="polite"
aria-relevant="all"
disabled={!isMeteorConnected}
value={currentSlideNum}
onChange={this.handleSkipToSlideChange}
data-test="skipSlide"
>
{this.renderSkipSlideOpts(numberOfSlides)}
</Styled.SkipSlideSelect>
</TooltipContainer>
<Styled.NextSlideButton
role="button"
aria-label={nextSlideAriaLabel}
aria-describedby={
endOfSlides ? 'noNextSlideDesc' : 'nextSlideDesc'
}
disabled={endOfSlides || !isMeteorConnected}
color="light"
circle
icon="right_arrow"
size="md"
onClick={this.nextSlideHandler}
label={intl.formatMessage(intlMessages.nextSlideLabel)}
hideLabel
data-test="nextSlide"
/>
</Styled.PresentationSlideControls>
<Styled.PresentationZoomControls>
<Styled.WBAccessButton
data-test={multiUser ? 'turnMultiUsersWhiteboardOff' : 'turnMultiUsersWhiteboardOn'}
role="button"
aria-label={
multiUser
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
}
color="light"
disabled={!isMeteorConnected}
icon={multiUser ? 'multi_whiteboard' : 'whiteboard'}
size="md"
circle
onClick={() => this.handleSwitchWhiteboardMode(!multiUser)}
label={
multiUser
? intl.formatMessage(intlMessages.toolbarMultiUserOff)
: intl.formatMessage(intlMessages.toolbarMultiUserOn)
}
hideLabel
/>
{multiUser ? (
<Styled.MultiUserTool
onClick={() => this.handleSwitchWhiteboardMode(!multiUser)}
>
{multiUserSize}
</Styled.MultiUserTool>
) : (
<Styled.MUTPlaceholder />
)}
{!isMobile ? (
<TooltipContainer>
<ZoomTool
slidePosition={slidePosition}
zoomValue={zoom}
currentSlideNum={currentSlideNum}
change={this.change}
minBound={HUNDRED_PERCENT}
maxBound={MAX_PERCENT}
step={STEP}
isMeteorConnected={isMeteorConnected}
/>
</TooltipContainer>
) : null}
<Styled.FitToWidthButton
role="button"
data-test="fitToWidthButton"
aria-describedby={fitToWidth ? 'fitPageDesc' : 'fitWidthDesc'}
aria-label={
fitToWidth
? `${intl.formatMessage(
intlMessages.presentationLabel,
)} ${intl.formatMessage(intlMessages.fitToPage)}`
: `${intl.formatMessage(
intlMessages.presentationLabel,
)} ${intl.formatMessage(intlMessages.fitToWidth)}`
}
color="light"
disabled={!isMeteorConnected}
icon="fit_to_width"
size="md"
circle
onClick={fitToWidthHandler}
label={fitToWidth
? intl.formatMessage(intlMessages.fitToPage)
: intl.formatMessage(intlMessages.fitToWidth)}
hideLabel
2023-08-14 21:47:23 +08:00
$fitToWidth={fitToWidth}
/>
</Styled.PresentationZoomControls>
2021-11-08 21:09:56 +08:00
</Styled.PresentationToolbarWrapper>
2016-08-03 06:55:20 +08:00
);
}
}
PresentationToolbar.propTypes = {
// Number of current slide being displayed
currentSlideNum: PropTypes.number.isRequired,
// Total number of slides in this presentation
numberOfSlides: PropTypes.number.isRequired,
// Actions required for the presenter toolbar
nextSlide: PropTypes.func.isRequired,
previousSlide: PropTypes.func.isRequired,
skipToSlide: PropTypes.func.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
zoomChanger: PropTypes.func.isRequired,
fitToWidthHandler: PropTypes.func.isRequired,
fitToWidth: PropTypes.bool.isRequired,
zoom: PropTypes.number.isRequired,
2019-06-27 00:29:34 +08:00
isMeteorConnected: PropTypes.bool.isRequired,
fullscreenElementId: PropTypes.string.isRequired,
fullscreenAction: PropTypes.string.isRequired,
isFullscreen: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
setIsPanning: PropTypes.func.isRequired,
multiUser: PropTypes.bool.isRequired,
whiteboardId: PropTypes.string.isRequired,
removeWhiteboardGlobalAccess: PropTypes.func.isRequired,
addWhiteboardGlobalAccess: PropTypes.func.isRequired,
fullscreenRef: PropTypes.instanceOf(Element),
handleToggleFullScreen: PropTypes.func.isRequired,
isPollingEnabled: PropTypes.bool.isRequired,
amIPresenter: PropTypes.bool.isRequired,
startPoll: PropTypes.func.isRequired,
currentSlide: PropTypes.shape().isRequired,
slidePosition: PropTypes.shape().isRequired,
multiUserSize: PropTypes.number.isRequired,
};
PresentationToolbar.defaultProps = {
fullscreenRef: null,
};
export default injectWbResizeEvent(injectIntl(PresentationToolbar));