diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx index c6fd754a13d16adf2afe0a3cc5975e58d80a9bf7..014280077617eebe7fd954508bc72964b7257a61 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/component.jsx @@ -43,19 +43,19 @@ const propTypes = { amIPresenter: PropTypes.bool.isRequired, }; -const getLocalizedAnswers = (type, intl) => { +const getLocalizedAnswers = (type, intl, pollTypes) => { switch (type) { - case 'TF': + case pollTypes.TrueFalse: return [ intl.formatMessage(intlMessages.trueOptionLabel), intl.formatMessage(intlMessages.falseOptionLabel), ]; - case 'YN': + case pollTypes.YesNo: return [ intl.formatMessage(intlMessages.yesOptionLabel), intl.formatMessage(intlMessages.noOptionLabel), ]; - case 'YNA': + case pollTypes.YesNoAbstention: return [ intl.formatMessage(intlMessages.yesOptionLabel), intl.formatMessage(intlMessages.noOptionLabel), @@ -66,18 +66,21 @@ const getLocalizedAnswers = (type, intl) => { } }; -const getAvailableQuickPolls = (slideId, parsedSlides, startPoll, intl) => { +const getAvailableQuickPolls = (slideId, parsedSlides, startPoll, intl, pollTypes) => { const pollItemElements = parsedSlides.map((poll) => { let { poll: label, type } = poll; let itemLabel = label; let answers = null; - if (type !== 'YN' && type !== 'YNA' && type !== 'TF') { + if (type !== pollTypes.YesNo && + type !== pollTypes.YesNoAbstention && + type !== pollTypes.TrueFalse) + { const { options } = itemLabel; itemLabel = options.join('/').replace(/[\n.)]/g, ''); } else { - answers = getLocalizedAnswers(type, intl); - type = 'custom'; + answers = getLocalizedAnswers(type, intl, pollTypes); + type = pollTypes.Custom; } // removes any whitespace from the label @@ -119,6 +122,7 @@ class QuickPollDropdown extends Component { currentSlide, activePoll, className, + pollTypes, } = this.props; const parsedSlide = parseCurrentSlideContent( @@ -130,7 +134,7 @@ class QuickPollDropdown extends Component { ); const { slideId, quickPollOptions } = parsedSlide; - const quickPolls = getAvailableQuickPolls(slideId, quickPollOptions, startPoll, intl); + const quickPolls = getAvailableQuickPolls(slideId, quickPollOptions, startPoll, intl, pollTypes); if (quickPollOptions.length === 0) return null; @@ -147,9 +151,12 @@ class QuickPollDropdown extends Component { singlePollType = type; } - if (singlePollType === 'TF' || singlePollType === 'YN' || singlePollType === 'YNA') { - answers = getLocalizedAnswers(singlePollType, intl); - singlePollType = 'custom'; + if (singlePollType === pollTypes.TrueFalse || + singlePollType === pollTypes.YesNo || + singlePollType === pollTypes.YesNoAbstention) + { + answers = getLocalizedAnswers(singlePollType, intl, pollTypes); + singlePollType = pollTypes.Custom; } let btn = ( diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx index 9cdb0071d645f6d0b3670d66ec5d0ed1a16692bf..e0d48e12846340e38604dc855c494691ace03634 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/quick-poll-dropdown/container.jsx @@ -2,9 +2,11 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { injectIntl } from 'react-intl'; import QuickPollDropdown from './component'; +import PollService from '/imports/ui/components/poll/service'; const QuickPollDropdownContainer = props => <QuickPollDropdown {...props} />; export default withTracker(() => ({ activePoll: Session.get('pollInitiated') || false, + pollTypes: PollService.pollTypes, }))(injectIntl(QuickPollDropdownContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index 20a3d3b9c351e0db77e45969826b9d71f8220eae..7a75ee3b307c562689272c3745c671a102589d33 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -227,17 +227,19 @@ class Poll extends Component { handleInputChange(e, index) { const { optList, type, error } = this.state; + const { pollTypes } = this.props; const list = [...optList]; const validatedVal = validateInput(e.target.value).replace(/\s{2,}/g, ' '); - const clearError = validatedVal.length > 0 && type !== 'R-'; + const clearError = validatedVal.length > 0 && type !== pollTypes.Response; list[index] = { val: validatedVal }; this.setState({ optList: list, error: clearError ? null : error }); } handleTextareaChange(e) { const { type, error } = this.state; + const { pollTypes } = this.props; const validatedQuestion = validateInput(e.target.value); - const clearError = validatedQuestion.length > 0 && type === 'R-'; + const clearError = validatedQuestion.length > 0 && type === pollTypes.Response; this.setState({ question: validateInput(e.target.value), error: clearError ? null : error }); } @@ -290,40 +292,8 @@ class Poll extends Component { } } - 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; - 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; - } - renderInputs() { - const { intl } = this.props; + const { intl, pollTypes } = this.props; const { optList, type, error } = this.state; let hasVal = false; return optList.map((o, i) => { @@ -363,7 +333,7 @@ class Poll extends Component { ) : <div style={{ width: '40px' }} />} </div> - {!hasVal && type !== 'R-' && error ? ( + {!hasVal && type !== pollTypes.Response && error ? ( <div className={styles.inputError}>{error}</div> ) : ( <div className={styles.errorSpacer}> </div> @@ -406,8 +376,8 @@ class Poll extends Component { const { type, optList, question, error, } = this.state; - const { startPoll, startCustomPoll, intl } = this.props; - const defaultPoll = type === 'TF' || type === 'A-' || type === 'YNA'; + const { startPoll, startCustomPoll, intl, pollTypes, isDefaultPoll, checkPollType } = this.props; + const defaultPoll = isDefaultPoll(type); return ( <div> <div className={styles.instructions}> @@ -425,7 +395,7 @@ class Poll extends Component { maxLength={QUESTION_MAX_INPUT_CHARS} placeholder={intl.formatMessage(intlMessages.questionLabel)} /> - {(type === 'R-' && question.length === 0 && error) ? ( + {(type === pollTypes.Response && question.length === 0 && error) ? ( <div className={styles.inputError}>{error}</div> ) : ( <div className={styles.errorSpacer}> </div> @@ -439,21 +409,21 @@ class Poll extends Component { color="default" onClick={() => { this.setState({ - type: 'TF', + type: pollTypes.TrueFalse, optList: [ { val: intl.formatMessage(intlMessages.true) }, { val: intl.formatMessage(intlMessages.false) }, ], }); }} - className={cx(styles.pBtn, styles.btnMR, { [styles.selectedBtnBlue]: type === 'TF' })} + className={cx(styles.pBtn, styles.btnMR, { [styles.selectedBtnBlue]: type === pollTypes.TrueFalse })} /> <Button label={intl.formatMessage(intlMessages.a4)} color="default" onClick={() => { this.setState({ - type: 'A-', + type: pollTypes.Letter, optList: [ { val: intl.formatMessage(intlMessages.a) }, { val: intl.formatMessage(intlMessages.b) }, @@ -462,7 +432,7 @@ class Poll extends Component { ], }); }} - className={cx(styles.pBtn, styles.btnML, { [styles.selectedBtnBlue]: type === 'A-' })} + className={cx(styles.pBtn, styles.btnML, { [styles.selectedBtnBlue]: type === pollTypes.Letter })} /> </div> <Button @@ -470,7 +440,7 @@ class Poll extends Component { color="default" onClick={() => { this.setState({ - type: 'YNA', + type: pollTypes.YesNoAbstention, optList: [ { val: intl.formatMessage(intlMessages.yes) }, { val: intl.formatMessage(intlMessages.no) }, @@ -478,13 +448,13 @@ class Poll extends Component { ], }); }} - className={cx(styles.pBtn, styles.yna, { [styles.selectedBtnBlue]: type === 'YNA' })} + className={cx(styles.pBtn, styles.yna, { [styles.selectedBtnBlue]: type === pollTypes.YesNoAbstention })} /> <Button label={intl.formatMessage(intlMessages.userResponse)} color="default" - onClick={() => { this.setState({ type: 'R-' }); }} - className={cx(styles.pBtn, styles.fullWidth, { [styles.selectedBtnWhite]: type === 'R-' })} + onClick={() => { this.setState({ type: pollTypes.Response }); }} + className={cx(styles.pBtn, styles.fullWidth, { [styles.selectedBtnWhite]: type === pollTypes.Response })} /> </div> { type @@ -492,7 +462,7 @@ class Poll extends Component { <div data-test="responseChoices"> <h4>{intl.formatMessage(intlMessages.responseChoices)}</h4> { - type === 'R-' + type === pollTypes.Response && ( <div> <span>{intl.formatMessage(intlMessages.typedResponseDesc)}</span> @@ -506,7 +476,7 @@ class Poll extends Component { ) } { - (defaultPoll || type === 'R-') + (defaultPoll || type === pollTypes.Response) && ( <div style={{ display: 'flex', @@ -538,17 +508,25 @@ class Poll extends Component { }); let err = null; - if (type === 'R-' && question.length === 0) err = intl.formatMessage(intlMessages.questionErr); - if (!hasVal && type !== 'R-') err = intl.formatMessage(intlMessages.optionErr); + if (type === pollTypes.Response && question.length === 0) err = intl.formatMessage(intlMessages.questionErr); + if (!hasVal && type !== pollTypes.Response) err = intl.formatMessage(intlMessages.optionErr); if (err) return this.setState({ error: err }); return this.setState({ isPolling: true }, () => { - const verifiedPollType = this.checkPollType(); + const verifiedPollType = checkPollType( + type, + optList, + intl.formatMessage(intlMessages.yes), + intl.formatMessage(intlMessages.no), + intl.formatMessage(intlMessages.abstention), + intl.formatMessage(intlMessages.true), + intl.formatMessage(intlMessages.false) + ); const verifiedOptions = optList.map((o) => { if (o.val.length > 0) return o.val; return null; }); - if (verifiedPollType === 'custom') { + if (verifiedPollType === pollTypes.Custom) { startCustomPoll( verifiedPollType, question, @@ -561,7 +539,7 @@ class Poll extends Component { }} /> { - FILE_DRAG_AND_DROP_ENABLED && type !== 'R-' && this.renderDragDrop() + FILE_DRAG_AND_DROP_ENABLED && type !== pollTypes.Response && this.renderDragDrop() } </div> ) @@ -675,7 +653,7 @@ Poll.propTypes = { formatMessage: PropTypes.func.isRequired, }).isRequired, amIPresenter: PropTypes.bool.isRequired, - pollTypes: PropTypes.instanceOf(Array).isRequired, + pollTypes: PropTypes.instanceOf(Object).isRequired, startPoll: PropTypes.func.isRequired, startCustomPoll: PropTypes.func.isRequired, stopPoll: PropTypes.func.isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/poll/container.jsx b/bigbluebutton-html5/imports/ui/components/poll/container.jsx index 45cd69ead5568d35189dcc431b12e04fdeba7f61..34a200b6061a55337cd95d16be30eb07eabc9025 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/container.jsx @@ -50,6 +50,8 @@ export default withTracker(() => { stopPoll, publishPoll: Service.publishPoll, currentPoll: Service.currentPoll(), + isDefaultPoll: Service.isDefaultPoll, + checkPollType: Service.checkPollType, resetPollPanel: Session.get('resetPollPanel') || false, pollAnswerIds: Service.pollAnswerIds, isMeteorConnected: Meteor.status().connected, diff --git a/bigbluebutton-html5/imports/ui/components/poll/service.js b/bigbluebutton-html5/imports/ui/components/poll/service.js index 2446a8350f0af8f1085f2f2104e1f5b94dcf2577..b3d7b39784dd8064ac5b1b8ae645a49fecae7788 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/service.js +++ b/bigbluebutton-html5/imports/ui/components/poll/service.js @@ -13,7 +13,18 @@ const MAX_POLL_RESULT_BARS = 20; // 'A-3' = A,B,C // 'A-4' = A,B,C,D // 'A-5' = A,B,C,D,E -const pollTypes = ['YN', 'YNA', 'TF', 'A-2', 'A-3', 'A-4', 'A-5', 'custom']; +const pollTypes = { + YesNo: 'YN', + YesNoAbstention: 'YNA', + TrueFalse: 'TF', + Letter: 'A-', + A2: 'A-2', + A3: 'A-3', + A4: 'A-4', + A5: 'A-5', + Custom: 'custom', + Response: 'R-', +} const pollAnswerIds = { true: { @@ -82,6 +93,63 @@ const getPollResultString = (isDefaultPoll, answers, numRespondents) => { return { resultString, optionsString }; } +const matchYesNoPoll = (yesValue, noValue, contentString) => { + const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`; + const ynOptionsRegex = new RegExp(ynPollString, 'gi'); + const ynPoll = contentString.match(ynOptionsRegex) || []; + return ynPoll; +} + +const matchYesNoAbstentionPoll = (yesValue, noValue, abstentionValue, contentString) => { + const ynaPollString = `(${yesValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${abstentionValue})|(${yesValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${yesValue})|(${noValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${abstentionValue})|(${noValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${yesValue})`; + const ynaOptionsRegex = new RegExp(ynaPollString, 'gi'); + const ynaPoll = contentString.match(ynaOptionsRegex) || []; + return ynaPoll; +} + +const matchTrueFalsePoll = (trueValue, falseValue, contentString) => { + const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`; + const tgOptionsRegex = new RegExp(tfPollString, 'gi'); + const tfPoll = contentString.match(tgOptionsRegex) || []; + return tfPoll; +} + +const checkPollType = (type, optList, yesValue, noValue, abstentionValue, trueValue, falseValue) => { + let _type = type; + let pollString = ''; + let defaultMatch = null; + let isDefault = null; + + switch (_type) { + case pollTypes.Letter: + 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}` : pollTypes.Custom; + break; + case pollTypes.TrueFalse: + pollString = optList.map((x) => x.val).join('/'); + defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString); + isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; + if (!isDefault) _type = pollTypes.Custom; + break; + case pollTypes.YesNoAbstention: + pollString = optList.map((x) => x.val).join('/'); + defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString); + isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; + if (!isDefault) { + // also try to match only yes/no + defaultMatch = matchYesNoPoll(yesValue, noValue, pollString);; + isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; + _type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom; + } + break; + default: + break; + } + return _type; +} + export default { amIPresenter: () => Users.findOne( { userId: Auth.userID }, @@ -91,6 +159,10 @@ export default { currentPoll: () => Polls.findOne({ meetingId: Auth.meetingID }), pollAnswerIds, POLL_AVATAR_COLOR, - isDefaultPoll: (pollType) => { return pollType !== 'custom' && pollType !== 'R-'}, + isDefaultPoll: (pollType) => { return pollType !== pollTypes.Custom && pollType !== pollTypes.Response}, getPollResultString: getPollResultString, + matchYesNoPoll: matchYesNoPoll, + matchYesNoAbstentionPoll: matchYesNoAbstentionPoll, + matchTrueFalsePoll: matchTrueFalsePoll, + checkPollType: checkPollType, }; diff --git a/bigbluebutton-html5/imports/ui/components/polling/component.jsx b/bigbluebutton-html5/imports/ui/components/polling/component.jsx index a47c684803ec375d60dbf16ce4e22d84fea153f1..2123a2da978a2846d5fd20016d6b93ec2112052d 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/polling/component.jsx @@ -92,6 +92,7 @@ class Polling extends Component { handleVote, handleTypedVote, pollAnswerIds, + pollTypes } = this.props; const { @@ -129,7 +130,7 @@ class Polling extends Component { ) } { - poll.pollType !== 'R-' && ( + poll.pollType !== pollTypes.Response && ( <span> { question.length === 0 @@ -184,7 +185,7 @@ class Polling extends Component { ) } { - poll.pollType === 'R-' + poll.pollType === pollTypes.Response && ( <div className={styles.typedResponseWrapper}> <input diff --git a/bigbluebutton-html5/imports/ui/components/polling/container.jsx b/bigbluebutton-html5/imports/ui/components/polling/container.jsx index 14f8455ce8eea77aceea510d14fa40a01c894d68..7f2c9ce1c0f6699e32a5f47a615bac0333cd1420 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/polling/container.jsx @@ -37,6 +37,7 @@ export default withTracker(() => { handleTypedVote, poll, pollAnswerIds: PollService.pollAnswerIds, + pollTypes: PollService.pollTypes, isMeteorConnected: Meteor.status().connected, }); })(PollingContainer); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/service.js b/bigbluebutton-html5/imports/ui/components/presentation/service.js index b04cab4388bb181a3eb595e74d4d1c94104c97b6..bf3ee758660ef5f9dbc428e99454b84e900317b1 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/service.js @@ -2,6 +2,7 @@ import PresentationPods from '/imports/api/presentation-pods'; import Presentations from '/imports/api/presentations'; import { Slides, SlidePositions } from '/imports/api/slides'; import Auth from '/imports/ui/services/auth'; +import PollService from '/imports/ui/components/poll/service'; const getCurrentPresentation = podId => Presentations.findOne({ podId, @@ -71,6 +72,7 @@ const currentSlidHasContent = () => { }; const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, falseValue) => { + const pollTypes = PollService.pollTypes; const currentSlide = getCurrentSlide('DEFAULT_PRESENTATION_POD'); const quickPollOptions = []; if (!currentSlide) return quickPollOptions; @@ -119,7 +121,7 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, }, []).filter(({ options, }) => options.length > 1 && options.length < 7).forEach(poll => quickPollOptions.push({ - type: `A-${poll.options.length}`, + type: `${pollTypes.Letter}${poll.options.length}`, poll, })); @@ -127,30 +129,22 @@ const parseCurrentSlideContent = (yesValue, noValue, abstentionValue, trueValue, content = content.replace(new RegExp(pollRegex), ''); } - const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`; - const ynOptionsRegex = new RegExp(ynPollString, 'gi'); - const ynPoll = content.match(ynOptionsRegex) || []; - - const ynaPollString = `(${yesValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${abstentionValue})|(${yesValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${yesValue})|(${noValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${abstentionValue})|(${noValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${yesValue})`; - const ynaOptionsRegex = new RegExp(ynaPollString, 'gi'); - const ynaPoll = content.match(ynaOptionsRegex) || []; - - const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`; - const tgOptionsRegex = new RegExp(tfPollString, 'gi'); - const tfPoll = content.match(tgOptionsRegex) || []; + const ynPoll = PollService.matchYesNoPoll(yesValue, noValue, content); + const ynaPoll = PollService.matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, content); + const tfPoll = PollService.matchTrueFalsePoll(trueValue, falseValue, content); ynPoll.forEach(poll => quickPollOptions.push({ - type: 'YN', + type: pollTypes.YesNo, poll, })); ynaPoll.forEach(poll => quickPollOptions.push({ - type: 'YNA', + type: pollTypes.YesNoAbstention, poll, })); tfPoll.forEach(poll => quickPollOptions.push({ - type: 'TF', + type: pollTypes.TrueFalse, poll, }));