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.

229 lines
5.3 KiB

  1. // Package imports.
  2. import classNames from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import React from 'react';
  5. import Overlay from 'react-overlays/lib/Overlay';
  6. // Components.
  7. import IconButton from 'flavours/glitch/components/icon_button';
  8. import ComposerOptionsDropdownContent from './content';
  9. // Utils.
  10. import { isUserTouching } from 'flavours/glitch/util/is_mobile';
  11. import { assignHandlers } from 'flavours/glitch/util/react_helpers';
  12. // Handlers.
  13. const handlers = {
  14. // Closes the dropdown.
  15. handleClose () {
  16. this.setState({ open: false });
  17. },
  18. // The enter key toggles the dropdown's open state, and the escape
  19. // key closes it.
  20. handleKeyDown ({ key }) {
  21. const {
  22. handleClose,
  23. handleToggle,
  24. } = this.handlers;
  25. switch (key) {
  26. case 'Enter':
  27. handleToggle(key);
  28. break;
  29. case 'Escape':
  30. handleClose();
  31. break;
  32. }
  33. },
  34. // Creates an action modal object.
  35. handleMakeModal () {
  36. const component = this;
  37. const {
  38. items,
  39. onChange,
  40. onModalOpen,
  41. onModalClose,
  42. value,
  43. } = this.props;
  44. // Required props.
  45. if (!(onChange && onModalOpen && onModalClose && items)) {
  46. return null;
  47. }
  48. // The object.
  49. return {
  50. actions: items.map(
  51. ({
  52. name,
  53. ...rest
  54. }) => ({
  55. ...rest,
  56. active: value && name === value,
  57. name,
  58. onClick (e) {
  59. e.preventDefault(); // Prevents focus from changing
  60. onModalClose();
  61. onChange(name);
  62. },
  63. onPassiveClick (e) {
  64. e.preventDefault(); // Prevents focus from changing
  65. onChange(name);
  66. component.setState({ needsModalUpdate: true });
  67. },
  68. })
  69. ),
  70. };
  71. },
  72. // Toggles opening and closing the dropdown.
  73. handleToggle ({ target }) {
  74. const { handleMakeModal } = this.handlers;
  75. const { onModalOpen } = this.props;
  76. const { open } = this.state;
  77. // If this is a touch device, we open a modal instead of the
  78. // dropdown.
  79. if (isUserTouching()) {
  80. // This gets the modal to open.
  81. const modal = handleMakeModal();
  82. // If we can, we then open the modal.
  83. if (modal && onModalOpen) {
  84. onModalOpen(modal);
  85. return;
  86. }
  87. }
  88. const { top } = target.getBoundingClientRect();
  89. this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
  90. // Otherwise, we just set our state to open.
  91. this.setState({ open: !open });
  92. },
  93. // If our modal is open and our props update, we need to also update
  94. // the modal.
  95. handleUpdate () {
  96. const { handleMakeModal } = this.handlers;
  97. const { onModalOpen } = this.props;
  98. const { needsModalUpdate } = this.state;
  99. // Gets our modal object.
  100. const modal = handleMakeModal();
  101. // Reopens the modal with the new object.
  102. if (needsModalUpdate && modal && onModalOpen) {
  103. onModalOpen(modal);
  104. }
  105. },
  106. };
  107. // The component.
  108. export default class ComposerOptionsDropdown extends React.PureComponent {
  109. // Constructor.
  110. constructor (props) {
  111. super(props);
  112. assignHandlers(this, handlers);
  113. this.state = {
  114. needsModalUpdate: false,
  115. open: false,
  116. placement: 'bottom',
  117. };
  118. }
  119. // Updates our modal as necessary.
  120. componentDidUpdate (prevProps) {
  121. const { handleUpdate } = this.handlers;
  122. const { items } = this.props;
  123. const { needsModalUpdate } = this.state;
  124. if (needsModalUpdate && items.find(
  125. (item, i) => item.on !== prevProps.items[i].on
  126. )) {
  127. handleUpdate();
  128. this.setState({ needsModalUpdate: false });
  129. }
  130. }
  131. // Rendering.
  132. render () {
  133. const {
  134. handleClose,
  135. handleKeyDown,
  136. handleToggle,
  137. } = this.handlers;
  138. const {
  139. active,
  140. disabled,
  141. title,
  142. icon,
  143. items,
  144. onChange,
  145. value,
  146. } = this.props;
  147. const { open, placement } = this.state;
  148. const computedClass = classNames('composer--options--dropdown', {
  149. active,
  150. open,
  151. top: placement === 'top',
  152. });
  153. // The result.
  154. return (
  155. <div
  156. className={computedClass}
  157. onKeyDown={handleKeyDown}
  158. >
  159. <IconButton
  160. active={open || active}
  161. className='value'
  162. disabled={disabled}
  163. icon={icon}
  164. onClick={handleToggle}
  165. size={18}
  166. style={{
  167. height: null,
  168. lineHeight: '27px',
  169. }}
  170. title={title}
  171. />
  172. <Overlay
  173. containerPadding={20}
  174. placement={placement}
  175. show={open}
  176. target={this}
  177. >
  178. <ComposerOptionsDropdownContent
  179. items={items}
  180. onChange={onChange}
  181. onClose={handleClose}
  182. value={value}
  183. />
  184. </Overlay>
  185. </div>
  186. );
  187. }
  188. }
  189. // Props.
  190. ComposerOptionsDropdown.propTypes = {
  191. active: PropTypes.bool,
  192. disabled: PropTypes.bool,
  193. icon: PropTypes.string,
  194. items: PropTypes.arrayOf(PropTypes.shape({
  195. icon: PropTypes.string,
  196. meta: PropTypes.node,
  197. name: PropTypes.string.isRequired,
  198. on: PropTypes.bool,
  199. text: PropTypes.node,
  200. })).isRequired,
  201. onChange: PropTypes.func,
  202. onModalClose: PropTypes.func,
  203. onModalOpen: PropTypes.func,
  204. title: PropTypes.string,
  205. value: PropTypes.string,
  206. };