diff --git a/bigbluebutton-html5/imports/ui/components/poll/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/component.jsx index 479238daf3a535469deafb983858a2435b24680a..5ff55f15e9d32531557f0cfc17b9684f67823e0d 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/poll/component.jsx @@ -9,6 +9,7 @@ import cx from 'classnames'; import Button from '/imports/ui/components/button/component'; import LiveResult from './live-result/component'; import { styles } from './styles.scss'; +import DragAndDrop from './dragAndDrop/component'; const intlMessages = defineMessages({ pollPaneTitle: { @@ -31,6 +32,10 @@ const intlMessages = defineMessages({ 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', @@ -148,6 +153,7 @@ const intlMessages = defineMessages({ const CHAT_ENABLED = Meteor.settings.public.chat.enabled; const MAX_CUSTOM_FIELDS = Meteor.settings.public.poll.max_custom; const MAX_INPUT_CHARS = 45; +const FILE_DRAG_AND_DROP_ENABLED = Meteor.settings.public.poll.allowDragAndDropFile; const validateInput = (i) => { let _input = i; @@ -206,6 +212,21 @@ class Poll extends Component { }); } + 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) { const { optList, type, error } = this.state; const list = [...optList]; @@ -222,6 +243,24 @@ class Poll extends Component { this.setState({ question: validateInput(e.target.value), error: clearError ? null : error }); } + pushToCustomPollValues(text) { + const lines = text.split('\n'); + 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]; @@ -496,6 +535,9 @@ class Poll extends Component { }); }} /> + { + FILE_DRAG_AND_DROP_ENABLED && this.renderDragDrop() + } </div> ) } @@ -537,6 +579,25 @@ class Poll extends Component { 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, diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..b31318d63f85cf7ac179fb68bf256a5796513ca4 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/component.jsx @@ -0,0 +1,143 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withModalMounter } from '/imports/ui/components/modal/service'; + +import { defineMessages, injectIntl } from 'react-intl'; +import { styles } from './styles.scss'; +import Button from '/imports/ui/components/button/component'; + + +// src: https://medium.com/@650egor/simple-drag-and-drop-file-upload-in-react-2cb409d88929 + +const intlMessages = defineMessages({ + customPollTextArea: { + id: 'app.poll.customPollTextArea', + description: 'label for button to submit custom poll values', + }, +}); + +class DragAndDrop extends Component { + static handleDrag(e) { + e.preventDefault(); + e.stopPropagation(); + } + + constructor(props) { + super(props); + + this.state = { + drag: false, + pollValueText: '', + }; + + this.dropRef = React.createRef(); + } + + componentDidMount() { + this.dragCounter = 0; + const div = this.dropRef.current; + div.addEventListener('dragenter', e => this.handleDragIn(e)); + div.addEventListener('dragleave', e => this.handleDragOut(e)); + div.addEventListener('dragover', e => DragAndDrop.handleDrag(e)); + div.addEventListener('drop', e => this.handleDrop(e)); + } + + componentWillUnmount() { + const div = this.dropRef.current; + div.removeEventListener('dragenter', e => this.handleDragIn(e)); + div.removeEventListener('dragleave', e => this.handleDragOut(e)); + div.removeEventListener('dragover', e => DragAndDrop.handleDrag(e)); + div.removeEventListener('drop', e => this.handleDrop(e)); + } + + setPollValues() { + const { pollValueText } = this.state; + const { handlePollValuesText } = this.props; + if (pollValueText) { + handlePollValuesText(pollValueText); + } + } + + setPollValuesFromFile(file) { + const reader = new FileReader(); + reader.onload = async (e) => { + const text = e.target.result; + this.setPollValueText(text); + this.setPollValues(); + }; + reader.readAsText(file); + } + + setPollValueText(pollText) { + const { MAX_INPUT_CHARS } = this.props; + const arr = pollText.split('\n'); + const text = arr.map(line => (line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line)).join('\n'); + this.setState({ pollValueText: text }); + } + + + handleTextInput(e) { + this.setPollValueText(e.target.value); + } + + + handleDragIn(e) { + DragAndDrop.handleDrag(e); + this.dragCounter += 1; + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + this.setState({ drag: true }); + } + } + + handleDragOut(e) { + DragAndDrop.handleDrag(e); + this.dragCounter -= 1; + if (this.dragCounter > 0) return; + this.setState({ drag: false }); + } + + handleDrop(e) { + DragAndDrop.handleDrag(e); + this.setState({ drag: false }); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + this.setPollValuesFromFile(e.dataTransfer.files[0]); + this.dragCounter = 0; + } + } + + + render() { + const { intl, children } = this.props; + const { pollValueText, drag } = this.state; + return ( + <div + className={styles.dndContainer} + ref={this.dropRef} + > + <textarea + value={pollValueText} + className={drag ? styles.dndActive : styles.dndInActive} + onChange={e => this.handleTextInput(e)} + /> + <Button + onClick={() => this.setPollValues()} + label={intl.formatMessage(intlMessages.customPollTextArea)} + color="primary" + disabled={pollValueText < 1} + className={styles.btn} + /> + {children} + </div> + + ); + } +} export default withModalMounter(injectIntl(DragAndDrop)); + +DragAndDrop.propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + MAX_INPUT_CHARS: PropTypes.number.isRequired, + handlePollValuesText: PropTypes.func.isRequired, + children: PropTypes.element.isRequired, +}; diff --git a/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss new file mode 100755 index 0000000000000000000000000000000000000000..abd06217ecf4e5d8ce4d9b48333dfc7ab4eb1b12 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/poll/dragAndDrop/styles.scss @@ -0,0 +1,26 @@ +@import "/imports/ui/stylesheets/variables/_all"; + +.dndContainer { + height: 200px; +} + +.customPollValuesTextfield { + width: 100%; + height: 100%; + resize: none; + font-size: var(--font-size-small); +} + +.dndActive { + @extend .customPollValuesTextfield; + background: grey; +} + +.dndInActive { + @extend .customPollValuesTextfield; + background: white; +} + +.btn { + width: 100%; +} diff --git a/bigbluebutton-html5/imports/ui/components/poll/styles.scss b/bigbluebutton-html5/imports/ui/components/poll/styles.scss index d2e1953cdb266325c91c8c6d45ced32528841697..775d7d23a23700f4938566666300363e47b9f21a 100644 --- a/bigbluebutton-html5/imports/ui/components/poll/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/poll/styles.scss @@ -356,3 +356,8 @@ color: var(--color-white) !important; } } + +.dragAndDropPollContainer { + width: 200px !important; + height: 200px !important; +} diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 4f0632eb0334f55ee0e7414fba8a3ee30c64d5f5..6ddbcff62bf1394fafd37756ffba2a44068bd6a9 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -291,6 +291,7 @@ public: poll: enabled: true max_custom: 5 + allowDragAndDropFile: false captions: enabled: true enableDictation: false diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json index 53555c2f9daf9d8546add45088bbbbc0e4cf9455..a2776b6914f13f8c6182961753fa6f71c630d896 100644 --- a/bigbluebutton-html5/private/locales/de.json +++ b/bigbluebutton-html5/private/locales/de.json @@ -219,6 +219,10 @@ "app.poll.hidePollDesc": "Versteckt das Umfragemenü", "app.poll.quickPollInstruction": "Wählen Sie eine der unten stehenden Optionen, um die Umfrage zu starten.", "app.poll.activePollInstruction": "Lassen Sie dieses Fenster offen, um auf die Antworten der Teilnehmer zu warten. Sobald Sie auf 'Umfrageergebnisse veröffentlichen' klicken, werden die Ergebnisse angezeigt und die Umfrage beendet.", + "app.poll.customPollLabel": "Benutzerdefinierte Umfrage", + "app.poll.startCustomLabel": "Benutzerdefinierte Umfrage starten", + "app.poll.dragDropPollInstruction": "Ziehen Sie per drag and Drop eine Textdatei mit den Umfrageoptionen auf das markierte Feld", + "app.poll.customPollTextArea": "Umfrageoptionen ausfüllen", "app.poll.publishLabel": "Umfrageergebnisse veröffentlichen", "app.poll.backLabel": "Umfrage starten", "app.poll.closeLabel": "Schließen", diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index b95310c4b815a7f97e5a700e16a4bef0f7b055fe..f8a034a5c634972c2a5beec2dc21c21cc0a1e0a1 100755 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -221,6 +221,8 @@ "app.poll.hidePollDesc": "Hides the poll menu pane", "app.poll.quickPollInstruction": "Select an option below to start your poll.", "app.poll.activePollInstruction": "Leave this panel open to see live responses to your poll. When you are ready, select 'Publish polling results' to publish the results and end the poll.", + "app.poll.dragDropPollInstruction": "To fill the poll values, drag a text file with the poll values onto the highlighted field", + "app.poll.customPollTextArea": "Fill poll values", "app.poll.publishLabel": "Publish polling results", "app.poll.backLabel": "Start A Poll", "app.poll.closeLabel": "Close",