123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- // @flow
-
- import React, { useCallback, useEffect, useRef, useState } from 'react';
-
- import { Icon, IconMenu } from '../../../base/icons';
- import { Tooltip } from '../../../base/tooltip';
- import { CHAR_LIMIT } from '../../constants';
- import AbstractPollCreate from '../AbstractPollCreate';
- import type { AbstractProps } from '../AbstractPollCreate';
-
- const PollCreate = (props: AbstractProps) => {
-
-
- const {
- addAnswer,
- answers,
- isSubmitDisabled,
- moveAnswer,
- onSubmit,
- question,
- removeAnswer,
- setAnswer,
- setCreateMode,
- setQuestion,
- t
- } = props;
-
- /*
- * This ref stores the Array of answer input fields, allowing us to focus on them.
- * This array is maintained by registerfieldRef and the useEffect below.
- */
- const answerInputs = useRef([]);
- const registerFieldRef = useCallback((i, r) => {
- if (r === null) {
- return;
- }
- answerInputs.current[i] = r;
- }, [ answerInputs ]);
-
- useEffect(() => {
- answerInputs.current = answerInputs.current.slice(0, answers.length);
- }, [ answers ]);
-
- /*
- * This state allows us to requestFocus asynchronously, without having to worry
- * about whether a newly created input field has been rendered yet or not.
- */
- const [ lastFocus, requestFocus ] = useState(null);
-
- useEffect(() => {
- if (lastFocus === null) {
- return;
- }
- const input = answerInputs.current[lastFocus];
-
- if (input === undefined) {
- return;
- }
- input.focus();
- }, [ lastFocus ]);
-
- const checkModifiers = useCallback(ev => {
- // Because this isn't done automatically on MacOS
- if (ev.key === 'Enter' && ev.metaKey) {
- ev.preventDefault();
- onSubmit();
-
- return;
- }
- if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
- return;
- }
- });
-
- const onQuestionKeyDown = useCallback(ev => {
- if (checkModifiers(ev)) {
- return;
- }
-
- if (ev.key === 'Enter') {
- requestFocus(0);
- ev.preventDefault();
- }
- });
-
- // Called on keypress in answer fields
- const onAnswerKeyDown = useCallback((i, ev) => {
- if (checkModifiers(ev)) {
- return;
- }
-
- if (ev.key === 'Enter') {
- addAnswer(i + 1);
- requestFocus(i + 1);
- ev.preventDefault();
- } else if (ev.key === 'Backspace' && ev.target.value === '' && answers.length > 1) {
- removeAnswer(i);
- requestFocus(i > 0 ? i - 1 : 0);
- ev.preventDefault();
- } else if (ev.key === 'ArrowDown') {
- if (i === answers.length - 1) {
- addAnswer();
- }
- requestFocus(i + 1);
- ev.preventDefault();
- } else if (ev.key === 'ArrowUp') {
- if (i === 0) {
- addAnswer(0);
- requestFocus(0);
- } else {
- requestFocus(i - 1);
- }
- ev.preventDefault();
- }
- }, [ answers, addAnswer, removeAnswer, requestFocus ]);
-
- const [ grabbing, setGrabbing ] = useState(null);
-
- const interchangeHeights = (i, j) => {
- const h = answerInputs.current[i].scrollHeight;
-
- answerInputs.current[i].style.height = `${answerInputs.current[j].scrollHeight}px`;
- answerInputs.current[j].style.height = `${h}px`;
- };
-
- const onGrab = useCallback((i, ev) => {
- if (ev.button !== 0) {
- return;
- }
- setGrabbing(i);
- window.addEventListener('mouseup', () => {
- setGrabbing(_grabbing => {
- requestFocus(_grabbing);
-
- return null;
- });
- }, { once: true });
- });
- const onMouseOver = useCallback(i => {
- if (grabbing !== null && grabbing !== i) {
- interchangeHeights(i, grabbing);
- moveAnswer(grabbing, i);
- setGrabbing(i);
- }
- });
-
- const autogrow = ev => {
- const el = ev.target;
-
- el.style.height = '1px';
- el.style.height = `${el.scrollHeight + 2}px`;
- };
-
- /* eslint-disable react/jsx-no-bind */
- return (<form
- className = 'polls-pane-content'
- onSubmit = { onSubmit }>
- <div className = 'poll-create-container poll-container'>
- <div className = 'poll-create-header'>
- { t('polls.create.create') }
- </div>
- <div className = 'poll-question-field'>
- <span className = 'poll-create-label'>
- { t('polls.create.pollQuestion') }
- </span>
- <textarea
- autoFocus = { true }
- className = 'expandable-input'
- maxLength = { CHAR_LIMIT }
- onChange = { ev => setQuestion(ev.target.value) }
- onInput = { autogrow }
- onKeyDown = { onQuestionKeyDown }
- placeholder = { t('polls.create.questionPlaceholder') }
- required = { true }
- row = '1'
- value = { question } />
- </div>
- <ol className = 'poll-answer-field-list'>
- {answers.map((answer, i) =>
- (<li
- className = { `poll-answer-field${grabbing === i ? ' poll-dragged' : ''}` }
- key = { i }
- onMouseOver = { () => onMouseOver(i) }>
- <span className = 'poll-create-label'>
- { t('polls.create.pollOption', { index: i + 1 })}
- </span>
- <div className = 'poll-create-option-row'>
- <textarea
- className = 'expandable-input'
- maxLength = { CHAR_LIMIT }
- onChange = { ev => setAnswer(i, ev.target.value) }
- onInput = { autogrow }
- onKeyDown = { ev => onAnswerKeyDown(i, ev) }
- placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
- ref = { r => registerFieldRef(i, r) }
- required = { true }
- row = { 1 }
- value = { answer } />
- <button
- className = 'poll-drag-handle'
- onMouseDown = { ev => onGrab(i, ev) }
- tabIndex = '-1'
- type = 'button'>
- <Icon src = { IconMenu } />
- </button>
- </div>
-
- { answers.length > 2
- && <Tooltip content = { t('polls.create.removeOption') }>
- <button
- className = 'poll-remove-option-button'
- onClick = { () => removeAnswer(i) }
- type = 'button'>
- { t('polls.create.removeOption') }
- </button>
- </Tooltip>}
- </li>)
- )}
- </ol>
- <div className = 'poll-add-button'>
- <button
- aria-label = { 'Add option' }
- className = 'poll-button poll-button-secondary'
- onClick = { () => {
- addAnswer();
- requestFocus(answers.length);
- } }
- type = 'button' >
- <span>{t('polls.create.addOption')}</span>
- </button>
- </div>
- </div>
- <div className = 'poll-footer poll-create-footer'>
- <button
- aria-label = { t('polls.create.cancel') }
- className = 'poll-button poll-button-secondary poll-button-short'
- onClick = { () => setCreateMode(false) }
- type = 'button' >
- <span>{t('polls.create.cancel')}</span>
- </button>
- <button
- aria-label = { t('polls.create.send') }
- className = 'poll-button poll-button-primary poll-button-short'
- disabled = { isSubmitDisabled }
- type = 'submit' >
- <span>{t('polls.create.send')}</span>
- </button>
- </div>
- </form>);
-
- };
-
- /*
- * We apply AbstractPollCreate to fill in the AbstractProps common
- * to both the web and native implementations.
- */
- // eslint-disable-next-line new-cap
- export default AbstractPollCreate(PollCreate);
|