Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

PollCreate.js 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // @flow
  2. import React, { useCallback, useEffect, useRef, useState } from 'react';
  3. import { Icon, IconMenu } from '../../../base/icons';
  4. import { Tooltip } from '../../../base/tooltip';
  5. import { CHAR_LIMIT } from '../../constants';
  6. import AbstractPollCreate from '../AbstractPollCreate';
  7. import type { AbstractProps } from '../AbstractPollCreate';
  8. const PollCreate = (props: AbstractProps) => {
  9. const {
  10. addAnswer,
  11. answers,
  12. isSubmitDisabled,
  13. moveAnswer,
  14. onSubmit,
  15. question,
  16. removeAnswer,
  17. setAnswer,
  18. setCreateMode,
  19. setQuestion,
  20. t
  21. } = props;
  22. /*
  23. * This ref stores the Array of answer input fields, allowing us to focus on them.
  24. * This array is maintained by registerfieldRef and the useEffect below.
  25. */
  26. const answerInputs = useRef([]);
  27. const registerFieldRef = useCallback((i, r) => {
  28. if (r === null) {
  29. return;
  30. }
  31. answerInputs.current[i] = r;
  32. }, [ answerInputs ]);
  33. useEffect(() => {
  34. answerInputs.current = answerInputs.current.slice(0, answers.length);
  35. }, [ answers ]);
  36. /*
  37. * This state allows us to requestFocus asynchronously, without having to worry
  38. * about whether a newly created input field has been rendered yet or not.
  39. */
  40. const [ lastFocus, requestFocus ] = useState(null);
  41. useEffect(() => {
  42. if (lastFocus === null) {
  43. return;
  44. }
  45. const input = answerInputs.current[lastFocus];
  46. if (input === undefined) {
  47. return;
  48. }
  49. input.focus();
  50. }, [ lastFocus ]);
  51. const checkModifiers = useCallback(ev => {
  52. // Composition events used to add accents to characters
  53. // despite their absence from standard US keyboards,
  54. // to build up logograms of many Asian languages
  55. // from their base components or categories and so on.
  56. if (ev.isComposing || ev.keyCode === 229) {
  57. // keyCode 229 means that user pressed some button,
  58. // but input method is still processing that.
  59. // This is a standard behavior for some input methods
  60. // like entering japanese or сhinese hieroglyphs.
  61. return true;
  62. }
  63. // Because this isn't done automatically on MacOS
  64. if (ev.key === 'Enter' && ev.metaKey) {
  65. ev.preventDefault();
  66. onSubmit();
  67. return;
  68. }
  69. if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
  70. return;
  71. }
  72. });
  73. const onQuestionKeyDown = useCallback(ev => {
  74. if (checkModifiers(ev)) {
  75. return;
  76. }
  77. if (ev.key === 'Enter') {
  78. requestFocus(0);
  79. ev.preventDefault();
  80. }
  81. });
  82. // Called on keypress in answer fields
  83. const onAnswerKeyDown = useCallback((i, ev) => {
  84. if (checkModifiers(ev)) {
  85. return;
  86. }
  87. if (ev.key === 'Enter') {
  88. // We add a new option input
  89. // only if we are on the last option input
  90. if (i === answers.length - 1) {
  91. addAnswer(i + 1);
  92. }
  93. requestFocus(i + 1);
  94. ev.preventDefault();
  95. } else if (ev.key === 'Backspace' && ev.target.value === '' && answers.length > 1) {
  96. removeAnswer(i);
  97. requestFocus(i > 0 ? i - 1 : 0);
  98. ev.preventDefault();
  99. } else if (ev.key === 'ArrowDown') {
  100. if (i === answers.length - 1) {
  101. addAnswer();
  102. }
  103. requestFocus(i + 1);
  104. ev.preventDefault();
  105. } else if (ev.key === 'ArrowUp') {
  106. if (i === 0) {
  107. addAnswer(0);
  108. requestFocus(0);
  109. } else {
  110. requestFocus(i - 1);
  111. }
  112. ev.preventDefault();
  113. }
  114. }, [ answers, addAnswer, removeAnswer, requestFocus ]);
  115. const [ grabbing, setGrabbing ] = useState(null);
  116. const interchangeHeights = (i, j) => {
  117. const h = answerInputs.current[i].scrollHeight;
  118. answerInputs.current[i].style.height = `${answerInputs.current[j].scrollHeight}px`;
  119. answerInputs.current[j].style.height = `${h}px`;
  120. };
  121. const onGrab = useCallback((i, ev) => {
  122. if (ev.button !== 0) {
  123. return;
  124. }
  125. setGrabbing(i);
  126. window.addEventListener('mouseup', () => {
  127. setGrabbing(_grabbing => {
  128. requestFocus(_grabbing);
  129. return null;
  130. });
  131. }, { once: true });
  132. });
  133. const onMouseOver = useCallback(i => {
  134. if (grabbing !== null && grabbing !== i) {
  135. interchangeHeights(i, grabbing);
  136. moveAnswer(grabbing, i);
  137. setGrabbing(i);
  138. }
  139. });
  140. const autogrow = ev => {
  141. const el = ev.target;
  142. el.style.height = '1px';
  143. el.style.height = `${el.scrollHeight + 2}px`;
  144. };
  145. /* eslint-disable react/jsx-no-bind */
  146. return (<form
  147. className = 'polls-pane-content'
  148. onSubmit = { onSubmit }>
  149. <div className = 'poll-create-container poll-container'>
  150. <div className = 'poll-create-header'>
  151. { t('polls.create.create') }
  152. </div>
  153. <div className = 'poll-question-field'>
  154. <span className = 'poll-create-label'>
  155. { t('polls.create.pollQuestion') }
  156. </span>
  157. <textarea
  158. autoFocus = { true }
  159. className = 'expandable-input'
  160. maxLength = { CHAR_LIMIT }
  161. onChange = { ev => setQuestion(ev.target.value) }
  162. onInput = { autogrow }
  163. onKeyDown = { onQuestionKeyDown }
  164. placeholder = { t('polls.create.questionPlaceholder') }
  165. required = { true }
  166. row = '1'
  167. value = { question } />
  168. </div>
  169. <ol className = 'poll-answer-field-list'>
  170. {answers.map((answer, i) =>
  171. (<li
  172. className = { `poll-answer-field${grabbing === i ? ' poll-dragged' : ''}` }
  173. key = { i }
  174. onMouseOver = { () => onMouseOver(i) }>
  175. <span className = 'poll-create-label'>
  176. { t('polls.create.pollOption', { index: i + 1 })}
  177. </span>
  178. <div className = 'poll-create-option-row'>
  179. <textarea
  180. className = 'expandable-input'
  181. maxLength = { CHAR_LIMIT }
  182. onChange = { ev => setAnswer(i, ev.target.value) }
  183. onInput = { autogrow }
  184. onKeyDown = { ev => onAnswerKeyDown(i, ev) }
  185. placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
  186. ref = { r => registerFieldRef(i, r) }
  187. required = { true }
  188. row = { 1 }
  189. value = { answer } />
  190. <button
  191. className = 'poll-drag-handle'
  192. onMouseDown = { ev => onGrab(i, ev) }
  193. tabIndex = '-1'
  194. type = 'button'>
  195. <Icon src = { IconMenu } />
  196. </button>
  197. </div>
  198. { answers.length > 2
  199. && <Tooltip content = { t('polls.create.removeOption') }>
  200. <button
  201. className = 'poll-remove-option-button'
  202. onClick = { () => removeAnswer(i) }
  203. type = 'button'>
  204. { t('polls.create.removeOption') }
  205. </button>
  206. </Tooltip>}
  207. </li>)
  208. )}
  209. </ol>
  210. <div className = 'poll-add-button'>
  211. <button
  212. aria-label = { 'Add option' }
  213. className = 'poll-button poll-button-secondary'
  214. onClick = { () => {
  215. addAnswer();
  216. requestFocus(answers.length);
  217. } }
  218. type = 'button' >
  219. <span>{t('polls.create.addOption')}</span>
  220. </button>
  221. </div>
  222. </div>
  223. <div className = 'poll-footer poll-create-footer'>
  224. <button
  225. aria-label = { t('polls.create.cancel') }
  226. className = 'poll-button poll-button-secondary poll-button-short'
  227. onClick = { () => setCreateMode(false) }
  228. type = 'button' >
  229. <span>{t('polls.create.cancel')}</span>
  230. </button>
  231. <button
  232. aria-label = { t('polls.create.send') }
  233. className = 'poll-button poll-button-primary poll-button-short'
  234. disabled = { isSubmitDisabled }
  235. type = 'submit' >
  236. <span>{t('polls.create.send')}</span>
  237. </button>
  238. </div>
  239. </form>);
  240. };
  241. /*
  242. * We apply AbstractPollCreate to fill in the AbstractProps common
  243. * to both the web and native implementations.
  244. */
  245. // eslint-disable-next-line new-cap
  246. export default AbstractPollCreate(PollCreate);