You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
6.7 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import ImmutablePureComponent from 'react-immutable-pure-component';
  5. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  6. import IconButton from 'mastodon/components/icon_button';
  7. import Icon from 'mastodon/components/icon';
  8. import AutosuggestInput from 'mastodon/components/autosuggest_input';
  9. import classNames from 'classnames';
  10. const messages = defineMessages({
  11. option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
  12. add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
  13. remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
  14. poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
  15. switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
  16. switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
  17. minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
  18. hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
  19. days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
  20. });
  21. @injectIntl
  22. class Option extends React.PureComponent {
  23. static propTypes = {
  24. title: PropTypes.string.isRequired,
  25. index: PropTypes.number.isRequired,
  26. isPollMultiple: PropTypes.bool,
  27. autoFocus: PropTypes.bool,
  28. onChange: PropTypes.func.isRequired,
  29. onRemove: PropTypes.func.isRequired,
  30. onToggleMultiple: PropTypes.func.isRequired,
  31. suggestions: ImmutablePropTypes.list,
  32. onClearSuggestions: PropTypes.func.isRequired,
  33. onFetchSuggestions: PropTypes.func.isRequired,
  34. onSuggestionSelected: PropTypes.func.isRequired,
  35. intl: PropTypes.object.isRequired,
  36. };
  37. handleOptionTitleChange = e => {
  38. this.props.onChange(this.props.index, e.target.value);
  39. };
  40. handleOptionRemove = () => {
  41. this.props.onRemove(this.props.index);
  42. };
  43. handleToggleMultiple = e => {
  44. this.props.onToggleMultiple();
  45. e.preventDefault();
  46. e.stopPropagation();
  47. };
  48. handleCheckboxKeypress = e => {
  49. if (e.key === 'Enter' || e.key === ' ') {
  50. this.handleToggleMultiple(e);
  51. }
  52. }
  53. onSuggestionsClearRequested = () => {
  54. this.props.onClearSuggestions();
  55. }
  56. onSuggestionsFetchRequested = (token) => {
  57. this.props.onFetchSuggestions(token);
  58. }
  59. onSuggestionSelected = (tokenStart, token, value) => {
  60. this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
  61. }
  62. render () {
  63. const { isPollMultiple, title, index, autoFocus, intl } = this.props;
  64. return (
  65. <li>
  66. <label className='poll__option editable'>
  67. <span
  68. className={classNames('poll__input', { checkbox: isPollMultiple })}
  69. onClick={this.handleToggleMultiple}
  70. onKeyPress={this.handleCheckboxKeypress}
  71. role='button'
  72. tabIndex='0'
  73. title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
  74. aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
  75. />
  76. <AutosuggestInput
  77. placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
  78. maxLength={50}
  79. value={title}
  80. onChange={this.handleOptionTitleChange}
  81. suggestions={this.props.suggestions}
  82. onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
  83. onSuggestionsClearRequested={this.onSuggestionsClearRequested}
  84. onSuggestionSelected={this.onSuggestionSelected}
  85. searchTokens={[':']}
  86. autoFocus={autoFocus}
  87. />
  88. </label>
  89. <div className='poll__cancel'>
  90. <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
  91. </div>
  92. </li>
  93. );
  94. }
  95. }
  96. export default
  97. @injectIntl
  98. class PollForm extends ImmutablePureComponent {
  99. static propTypes = {
  100. options: ImmutablePropTypes.list,
  101. expiresIn: PropTypes.number,
  102. isMultiple: PropTypes.bool,
  103. onChangeOption: PropTypes.func.isRequired,
  104. onAddOption: PropTypes.func.isRequired,
  105. onRemoveOption: PropTypes.func.isRequired,
  106. onChangeSettings: PropTypes.func.isRequired,
  107. suggestions: ImmutablePropTypes.list,
  108. onClearSuggestions: PropTypes.func.isRequired,
  109. onFetchSuggestions: PropTypes.func.isRequired,
  110. onSuggestionSelected: PropTypes.func.isRequired,
  111. intl: PropTypes.object.isRequired,
  112. };
  113. handleAddOption = () => {
  114. this.props.onAddOption('');
  115. };
  116. handleSelectDuration = e => {
  117. this.props.onChangeSettings(e.target.value, this.props.isMultiple);
  118. };
  119. handleToggleMultiple = () => {
  120. this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
  121. };
  122. render () {
  123. const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
  124. if (!options) {
  125. return null;
  126. }
  127. const autoFocusIndex = options.indexOf('');
  128. return (
  129. <div className='compose-form__poll-wrapper'>
  130. <ul>
  131. {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
  132. </ul>
  133. <div className='poll__footer'>
  134. <button disabled={options.size >= 10} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
  135. {/* eslint-disable-next-line jsx-a11y/no-onchange */}
  136. <select value={expiresIn} onChange={this.handleSelectDuration}>
  137. <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
  138. <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
  139. <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
  140. <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
  141. <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
  142. <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
  143. <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
  144. </select>
  145. </div>
  146. </div>
  147. );
  148. }
  149. }