bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/poll/component.jsx

677 lines
20 KiB
React
Raw Normal View History

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container';
import { withModalMounter } from '/imports/ui/components/modal/service';
import _ from 'lodash';
2018-10-18 22:31:17 +08:00
import { Session } from 'meteor/session';
import cx from 'classnames';
2018-09-15 01:50:18 +08:00
import Button from '/imports/ui/components/button/component';
2018-10-29 23:27:50 +08:00
import LiveResult from './live-result/component';
2018-09-15 01:50:18 +08:00
import { styles } from './styles.scss';
import DragAndDrop from './dragAndDrop/component';
2018-09-15 01:50:18 +08:00
const intlMessages = defineMessages({
pollPaneTitle: {
id: 'app.poll.pollPaneTitle',
description: 'heading label for the poll menu',
},
2018-10-30 23:57:10 +08:00
closeLabel: {
id: 'app.poll.closeLabel',
description: 'label for poll pane close button',
},
hidePollDesc: {
id: 'app.poll.hidePollDesc',
description: 'aria label description for hide poll button',
},
quickPollInstruction: {
id: 'app.poll.quickPollInstruction',
description: 'instructions for using pre configured polls',
},
2018-09-25 06:43:54 +08:00
activePollInstruction: {
id: 'app.poll.activePollInstruction',
description: 'instructions displayed when a poll is active',
},
dragDropPollInstruction: {
id: 'app.poll.dragDropPollInstruction',
description: 'instructions for upload poll options via drag and drop',
},
ariaInputCount: {
id: 'app.poll.ariaInputCount',
description: 'aria label for custom poll input field',
},
customPlaceholder: {
id: 'app.poll.customPlaceholder',
description: 'custom poll input field placeholder text',
},
noPresentationSelected: {
id: 'app.poll.noPresentationSelected',
description: 'no presentation label',
},
clickHereToSelect: {
id: 'app.poll.clickHereToSelect',
description: 'open uploader modal button label',
},
2020-09-22 06:52:38 +08:00
questionErr: {
id: 'app.poll.questionErr',
description: 'question text area error label',
},
optionErr: {
id: 'app.poll.optionErr',
description: 'poll input error label',
},
tf: {
id: 'app.poll.tf',
description: 'label for true / false poll',
},
a4: {
id: 'app.poll.a4',
description: 'label for A / B / C / D poll',
},
delete: {
id: 'app.poll.optionDelete.label',
description: '',
},
pollPanelDesc: {
id: 'app.poll.panel.desc',
description: '',
},
questionLabel: {
id: 'app.poll.question.label',
description: '',
},
userResponse: {
id: 'app.poll.userResponse.label',
description: '',
},
responseChoices: {
id: 'app.poll.responseChoices.label',
description: '',
},
typedResponseDesc: {
id: 'app.poll.typedResponse.desc',
description: '',
},
responseTypesLabel: {
id: 'app.poll.responseTypes.label',
description: '',
},
addOptionLabel: {
id: 'app.poll.addItem.label',
description: '',
},
startPollLabel: {
id: 'app.poll.start.label',
description: '',
},
questionTitle: {
id: 'app.poll.question.title',
description: '',
},
true: {
id: 'app.poll.answer.true',
description: '',
},
false: {
id: 'app.poll.answer.false',
description: '',
},
a: {
id: 'app.poll.answer.a',
description: '',
},
b: {
id: 'app.poll.answer.b',
description: '',
},
c: {
id: 'app.poll.answer.c',
description: '',
},
d: {
id: 'app.poll.answer.d',
description: '',
},
2021-02-19 00:06:21 +08:00
yna: {
id: 'app.poll.yna',
description: '',
},
yes: {
id: 'app.poll.y',
description: '',
},
no: {
id: 'app.poll.n',
description: '',
},
abstention: {
id: 'app.poll.abstention',
description: '',
},
});
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
2018-10-03 07:59:45 +08:00
const MAX_CUSTOM_FIELDS = Meteor.settings.public.poll.max_custom;
2019-04-02 12:07:45 +08:00
const MAX_INPUT_CHARS = 45;
const FILE_DRAG_AND_DROP_ENABLED = Meteor.settings.public.poll.allowDragAndDropFile;
2018-10-03 07:59:45 +08:00
const validateInput = (i) => {
let _input = i;
if (/^\s/.test(_input)) _input = '';
return _input;
};
class Poll extends Component {
2018-09-15 01:50:18 +08:00
constructor(props) {
super(props);
this.state = {
2018-09-25 06:43:54 +08:00
isPolling: false,
question: '',
optList: [],
2020-09-22 06:52:38 +08:00
error: null,
2018-09-15 01:50:18 +08:00
};
this.handleBackClick = this.handleBackClick.bind(this);
this.handleAddOption = this.handleAddOption.bind(this);
this.handleRemoveOption = this.handleRemoveOption.bind(this);
this.handleTextareaChange = this.handleTextareaChange.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
2019-05-07 04:18:23 +08:00
componentDidMount() {
const { props } = this.hideBtn;
const { className } = props;
const hideBtn = document.getElementsByClassName(`${className}`);
if (hideBtn[0]) hideBtn[0].focus();
2019-05-07 04:18:23 +08:00
}
2018-11-23 12:08:48 +08:00
componentDidUpdate() {
const { amIPresenter } = this.props;
2018-11-23 12:08:48 +08:00
if (Session.equals('resetPollPanel', true)) {
2019-03-15 03:34:53 +08:00
this.handleBackClick();
}
if (!amIPresenter) {
2018-11-23 12:08:48 +08:00
Session.set('openPanel', 'userlist');
Session.set('forcePollOpen', false);
}
}
2018-12-18 23:15:51 +08:00
handleBackClick() {
const { stopPoll } = this.props;
this.setState({
isPolling: false,
2020-09-22 06:52:38 +08:00
error: null,
}, () => {
stopPoll();
Session.set('resetPollPanel', false);
document.activeElement.blur();
});
}
handleInputChange(index, event) {
this.handleInputTextChange(index, event.target.value);
}
handleInputTextChange(index, text) {
const { optList } = this.state;
// This regex will replace any instance of 2 or more consecutive white spaces
// with a single white space character.
const option = text.replace(/\s{2,}/g, ' ').trim();
if (index < optList.length) optList[index].val = option === '' ? '' : option;
this.setState({ optList });
}
handleInputChange(e, index) {
2020-09-22 06:52:38 +08:00
const { optList, type, error } = this.state;
const list = [...optList];
2020-09-22 06:52:38 +08:00
const validatedVal = validateInput(e.target.value).replace(/\s{2,}/g, ' ');
const clearError = validatedVal.length > 0 && type !== 'RP';
list[index] = { val: validatedVal };
this.setState({ optList: list, error: clearError ? null : error });
}
handleTextareaChange(e) {
2020-09-22 06:52:38 +08:00
const { type, error } = this.state;
const validatedQuestion = validateInput(e.target.value);
const clearError = validatedQuestion.length > 0 && type === 'RP';
this.setState({ question: validateInput(e.target.value), error: clearError ? null : error });
}
setOptListLength(len) {
const { optList } = this.state;
len = len > MAX_CUSTOM_FIELDS ? MAX_CUSTOM_FIELDS : len;
const diff = len - optList.length;
if (diff > 0) {
const emptyAddition = Array(diff).fill({ val: '' });
optList.push(...emptyAddition);
} else {
optList.splice(len);
}
this.setState({ optList });
}
pushToCustomPollValues(text) {
const lines = text.split('\n');
this.setOptListLength(lines.length);
for (let i = 0; i < MAX_CUSTOM_FIELDS; i += 1) {
let line = '';
if (i < lines.length) {
line = lines[i];
line = line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line;
}
this.handleInputTextChange(i, line);
}
}
handlePollValuesText(text) {
if (text && text.length > 0) {
this.pushToCustomPollValues(text);
}
}
handleRemoveOption(index) {
const { optList } = this.state;
const list = [...optList];
list.splice(index, 1);
this.setState({ optList: list });
2018-09-15 01:50:18 +08:00
}
handleAddOption() {
const { optList } = this.state;
this.setState({ optList: [...optList, { val: '' }] });
}
checkPollType() {
const { type, optList } = this.state;
let _type = type;
let pollString = '';
let defaultMatch = null;
let isDefault = null;
switch (_type) {
case 'A-':
pollString = optList.map(x => x.val).sort().join('');
defaultMatch = pollString.match(/^(ABCDEFG)|(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi);
isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
_type = isDefault ? `${_type}${defaultMatch[0].length}` : 'custom';
break;
case 'TF':
pollString = optList.map(x => x.val).join('');
defaultMatch = pollString.match(/^(TRUEFALSE)|(FALSETRUE)$/gi);
isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
if (!isDefault) _type = 'custom';
break;
2021-02-19 00:06:21 +08:00
case 'YNA':
pollString = optList.map(x => x.val).join('');
defaultMatch = pollString.match(/^(YesNoAbstention)$/gi);
isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
if (!isDefault) _type = 'custom';
break;
default:
break;
}
return _type;
2018-09-15 01:50:18 +08:00
}
renderInputs() {
2018-12-18 23:15:51 +08:00
const { intl } = this.props;
2020-09-22 06:52:38 +08:00
const { optList, type, error } = this.state;
let hasVal = false;
return optList.map((o, i) => {
2020-09-22 06:52:38 +08:00
if (o.val.length > 0) hasVal = true;
const pollOptionKey = `poll-option-${i}`;
2018-12-18 23:15:51 +08:00
return (
2020-09-22 06:52:38 +08:00
<span>
<div
key={pollOptionKey}
style={{
display: 'flex',
justifyContent: 'spaceBetween',
}}
>
<input
type="text"
value={o.val}
placeholder={intl.formatMessage(intlMessages.customPlaceholder)}
data-test="pollOptionItem"
2020-09-22 06:52:38 +08:00
className={styles.pollOption}
onChange={e => this.handleInputChange(e, i)}
maxLength={MAX_INPUT_CHARS}
/>
{ i > 1 ? (
<Button
className={styles.deleteBtn}
label={intl.formatMessage(intlMessages.delete)}
icon="delete"
data-test="deletePollOption"
2020-09-22 06:52:38 +08:00
hideLabel
circle
color="default"
onClick={() => {
this.handleRemoveOption(i);
}}
/>) : <div style={{ width: '40px' }} />
}
2020-09-22 06:52:38 +08:00
</div>
{!hasVal && type !== 'RP' && error ? (
<div className={styles.inputError}>{error}</div>
) : (
<div className={styles.errorSpacer}>&nbsp;</div>
)}
</span>
2018-12-18 23:15:51 +08:00
);
});
2018-10-24 22:17:13 +08:00
}
2018-09-25 06:43:54 +08:00
renderActivePollOptions() {
const {
2019-05-23 02:00:44 +08:00
intl,
isMeteorConnected,
2019-05-23 02:00:44 +08:00
stopPoll,
currentPoll,
pollAnswerIds,
2018-09-25 06:43:54 +08:00
} = this.props;
return (
<div>
2018-10-11 02:25:35 +08:00
<div className={styles.instructions}>
{intl.formatMessage(intlMessages.activePollInstruction)}
</div>
2018-10-29 23:27:50 +08:00
<LiveResult
{...{
isMeteorConnected,
2018-10-29 23:27:50 +08:00
stopPoll,
currentPoll,
2019-05-23 02:00:44 +08:00
pollAnswerIds,
2018-10-29 23:27:50 +08:00
}}
handleBackClick={this.handleBackClick}
2018-10-29 23:27:50 +08:00
/>
2018-09-25 06:43:54 +08:00
</div>
);
}
renderPollOptions() {
2020-09-22 06:52:38 +08:00
const {
type, optList, question, error,
} = this.state;
const { startPoll, startCustomPoll, intl } = this.props;
2021-02-19 00:06:21 +08:00
const defaultPoll = type === 'TF' || type === 'A-' || type === 'YNA';
2018-09-15 01:50:18 +08:00
return (
2018-09-24 06:20:20 +08:00
<div>
<div className={styles.instructions}>
{intl.formatMessage(intlMessages.pollPanelDesc)}
2018-09-15 01:50:18 +08:00
</div>
<div>
<h4>{intl.formatMessage(intlMessages.questionTitle)}</h4>
<textarea
data-test="pollQuestionArea"
className={styles.pollQuestion}
value={question}
onChange={e => this.handleTextareaChange(e)}
rows="4"
cols="35"
placeholder={intl.formatMessage(intlMessages.questionLabel)}
/>
2020-09-22 06:52:38 +08:00
{(type === 'RP' && question.length === 0 && error) ? (
<div className={styles.inputError}>{error}</div>
) : (
<div className={styles.errorSpacer}>&nbsp;</div>
)}
</div>
<div data-test="responseTypes">
<h4>{intl.formatMessage(intlMessages.responseTypesLabel)}</h4>
<div className={styles.responseType}>
<Button
label={intl.formatMessage(intlMessages.tf)}
color="default"
onClick={() => {
this.setState({
type: 'TF',
optList: [
{ val: intl.formatMessage(intlMessages.true) },
{ val: intl.formatMessage(intlMessages.false) },
],
});
}}
className={cx(styles.pBtn, { [styles.selectedBtn]: type === 'TF' })}
/>
<Button
label={intl.formatMessage(intlMessages.a4)}
color="default"
onClick={() => {
this.setState({
type: 'A-',
optList: [
{ val: intl.formatMessage(intlMessages.a) },
{ val: intl.formatMessage(intlMessages.b) },
{ val: intl.formatMessage(intlMessages.c) },
{ val: intl.formatMessage(intlMessages.d) },
],
});
}}
className={cx(styles.pBtn, { [styles.selectedBtn]: type === 'A-' })}
/>
</div>
2021-02-19 00:06:21 +08:00
<Button
label={intl.formatMessage(intlMessages.yna)}
color="default"
onClick={() => {
this.setState({
type: 'YNA',
optList: [
{ val: intl.formatMessage(intlMessages.yes) },
{ val: intl.formatMessage(intlMessages.no) },
{ val: intl.formatMessage(intlMessages.abstention) },
],
});
}}
className={cx(styles.pBtn, styles.yna, { [styles.selectedBtn]: type === 'YNA' })}
/>
<Button
label={intl.formatMessage(intlMessages.userResponse)}
color="default"
onClick={() => { this.setState({ type: 'RP' }); }}
className={cx(styles.pBtn, styles.fullWidth, { [styles.selectedBtn]: type === 'RP' })}
/>
</div>
{ type
&& (
<div data-test="responseChoices">
<h4>{intl.formatMessage(intlMessages.responseChoices)}</h4>
{
type === 'RP'
&& (
<div>
<span>{intl.formatMessage(intlMessages.typedResponseDesc)}</span>
<div className={styles.exampleResponse}>
<div className={styles.exampleTitle} />
<div className={styles.responseInput}>
<div className={styles.rInput} />
</div>
</div>
</div>
)
}
{
(defaultPoll || type === 'RP')
&& (
<div style={{
display: 'flex',
flexFlow: 'column',
}}
>
{defaultPoll && this.renderInputs()}
{defaultPoll
&& (
<Button
className={styles.addItemBtn}
data-test="addItem"
label={intl.formatMessage(intlMessages.addOptionLabel)}
color="default"
icon="add"
disabled={optList.length === MAX_CUSTOM_FIELDS}
onClick={() => this.handleAddOption()}
/>
)
}
<Button
className={styles.startPollBtn}
data-test="startPoll"
label={intl.formatMessage(intlMessages.startPollLabel)}
color="primary"
onClick={() => {
2020-09-22 06:52:38 +08:00
let hasVal = false;
optList.forEach((o) => {
if (o.val.length > 0) hasVal = true;
});
let err = null;
if (type === 'RP' && question.length === 0) err = intl.formatMessage(intlMessages.questionErr);
if (!hasVal && type !== 'RP') err = intl.formatMessage(intlMessages.optionErr);
if (err) return this.setState({ error: err });
this.setState({ isPolling: true }, () => {
const verifiedPollType = this.checkPollType();
const verifiedOptions = optList.map((o) => {
if (o.val.length > 0) return o.val;
return null;
});
if (verifiedPollType === 'custom') {
startCustomPoll(
verifiedPollType,
question,
_.compact(verifiedOptions),
);
} else {
startPoll(verifiedPollType, question);
}
});
}}
/>
{
FILE_DRAG_AND_DROP_ENABLED && type !== 'RP' && this.renderDragDrop()
}
</div>
)
}
</div>
)
}
2018-09-15 01:50:18 +08:00
</div>
);
}
2018-09-25 06:43:54 +08:00
2019-03-15 03:34:53 +08:00
renderNoSlidePanel() {
const { mountModal, intl } = this.props;
return (
<div className={styles.noSlidePanelContainer}>
<h4>{intl.formatMessage(intlMessages.noPresentationSelected)}</h4>
<Button
label={intl.formatMessage(intlMessages.clickHereToSelect)}
color="primary"
onClick={() => mountModal(<PresentationUploaderContainer />)}
className={styles.pollBtn}
/>
</div>
);
}
2019-03-15 03:34:53 +08:00
renderPollPanel() {
const { isPolling } = this.state;
2018-09-25 06:43:54 +08:00
const {
currentPoll,
currentSlide,
2018-09-25 06:43:54 +08:00
} = this.props;
if (!CHAT_ENABLED && !currentSlide) return this.renderNoSlidePanel();
if (isPolling || (!isPolling && currentPoll)) {
return this.renderActivePollOptions();
}
return this.renderPollOptions();
}
renderDragDrop() {
const { intl } = this.props;
return (
<div>
<div className={styles.instructions}>
{intl.formatMessage(intlMessages.dragDropPollInstruction)}
</div>
<DragAndDrop
{...{ intl, MAX_INPUT_CHARS }}
handlePollValuesText={e => this.handlePollValuesText(e)}
>
<div className={styles.dragAndDropPollContainer} />
</DragAndDrop>
</div>
);
}
render() {
const {
intl,
stopPoll,
currentPoll,
amIPresenter,
} = this.props;
2018-12-18 23:15:51 +08:00
if (!amIPresenter) return null;
2018-11-23 12:08:48 +08:00
2018-09-25 06:43:54 +08:00
return (
<div>
<header className={styles.header}>
2018-10-18 22:31:17 +08:00
<Button
2019-05-07 04:18:23 +08:00
ref={(node) => { this.hideBtn = node; }}
2020-03-20 22:42:04 +08:00
data-test="hidePollDesc"
2018-10-18 22:31:17 +08:00
tabIndex={0}
label={intl.formatMessage(intlMessages.pollPaneTitle)}
icon="left_arrow"
2018-09-25 06:43:54 +08:00
aria-label={intl.formatMessage(intlMessages.hidePollDesc)}
2018-10-18 22:31:17 +08:00
className={styles.hideBtn}
onClick={() => { Session.set('openPanel', 'userlist'); }}
/>
2018-10-24 22:17:13 +08:00
<Button
2018-10-30 23:57:10 +08:00
label={intl.formatMessage(intlMessages.closeLabel)}
aria-label={`${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`}
2018-10-24 22:17:13 +08:00
onClick={() => {
if (currentPoll) stopPoll();
2018-12-18 23:15:51 +08:00
Session.set('openPanel', 'userlist');
Session.set('forcePollOpen', false);
Session.set('pollInitiated', false);
2018-12-18 23:15:51 +08:00
}}
2018-10-24 22:17:13 +08:00
className={styles.closeBtn}
2018-10-30 00:14:05 +08:00
icon="close"
size="sm"
2018-10-30 23:57:10 +08:00
hideLabel
2018-10-24 22:17:13 +08:00
/>
2018-09-25 06:43:54 +08:00
</header>
{this.renderPollPanel()}
2018-09-25 06:43:54 +08:00
</div>
);
}
2018-09-15 01:50:18 +08:00
}
export default withModalMounter(injectIntl(Poll));
Poll.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
amIPresenter: PropTypes.bool.isRequired,
pollTypes: PropTypes.instanceOf(Array).isRequired,
startPoll: PropTypes.func.isRequired,
startCustomPoll: PropTypes.func.isRequired,
stopPoll: PropTypes.func.isRequired,
};