column_header.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { createPortal } from 'react-dom';
  4. import classNames from 'classnames';
  5. import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
  6. import Icon from 'mastodon/components/icon';
  7. const messages = defineMessages({
  8. show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
  9. hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
  10. moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
  11. moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
  12. });
  13. export default @injectIntl
  14. class ColumnHeader extends React.PureComponent {
  15. static contextTypes = {
  16. router: PropTypes.object,
  17. identity: PropTypes.object,
  18. };
  19. static propTypes = {
  20. intl: PropTypes.object.isRequired,
  21. title: PropTypes.node,
  22. icon: PropTypes.string,
  23. active: PropTypes.bool,
  24. multiColumn: PropTypes.bool,
  25. extraButton: PropTypes.node,
  26. showBackButton: PropTypes.bool,
  27. children: PropTypes.node,
  28. pinned: PropTypes.bool,
  29. placeholder: PropTypes.bool,
  30. onPin: PropTypes.func,
  31. onMove: PropTypes.func,
  32. onClick: PropTypes.func,
  33. appendContent: PropTypes.node,
  34. collapseIssues: PropTypes.bool,
  35. };
  36. state = {
  37. collapsed: true,
  38. animating: false,
  39. };
  40. historyBack = () => {
  41. if (window.history && window.history.length === 1) {
  42. this.context.router.history.push('/');
  43. } else {
  44. this.context.router.history.goBack();
  45. }
  46. }
  47. handleToggleClick = (e) => {
  48. e.stopPropagation();
  49. this.setState({ collapsed: !this.state.collapsed, animating: true });
  50. }
  51. handleTitleClick = () => {
  52. this.props.onClick();
  53. }
  54. handleMoveLeft = () => {
  55. this.props.onMove(-1);
  56. }
  57. handleMoveRight = () => {
  58. this.props.onMove(1);
  59. }
  60. handleBackClick = () => {
  61. this.historyBack();
  62. }
  63. handleTransitionEnd = () => {
  64. this.setState({ animating: false });
  65. }
  66. handlePin = () => {
  67. if (!this.props.pinned) {
  68. this.context.router.history.replace('/');
  69. }
  70. this.props.onPin();
  71. }
  72. render () {
  73. const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
  74. const { collapsed, animating } = this.state;
  75. const wrapperClassName = classNames('column-header__wrapper', {
  76. 'active': active,
  77. });
  78. const buttonClassName = classNames('column-header', {
  79. 'active': active,
  80. });
  81. const collapsibleClassName = classNames('column-header__collapsible', {
  82. 'collapsed': collapsed,
  83. 'animating': animating,
  84. });
  85. const collapsibleButtonClassName = classNames('column-header__button', {
  86. 'active': !collapsed,
  87. });
  88. let extraContent, pinButton, moveButtons, backButton, collapseButton;
  89. if (children) {
  90. extraContent = (
  91. <div key='extra-content' className='column-header__collapsible__extra'>
  92. {children}
  93. </div>
  94. );
  95. }
  96. if (multiColumn && pinned) {
  97. pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
  98. moveButtons = (
  99. <div key='move-buttons' className='column-header__setting-arrows'>
  100. <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
  101. <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
  102. </div>
  103. );
  104. } else if (multiColumn && this.props.onPin) {
  105. pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
  106. }
  107. if (!pinned && (multiColumn || showBackButton)) {
  108. backButton = (
  109. <button onClick={this.handleBackClick} className='column-header__back-button'>
  110. <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
  111. <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
  112. </button>
  113. );
  114. }
  115. const collapsedContent = [
  116. extraContent,
  117. ];
  118. if (multiColumn) {
  119. collapsedContent.push(pinButton);
  120. collapsedContent.push(moveButtons);
  121. }
  122. if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
  123. collapseButton = (
  124. <button
  125. className={collapsibleButtonClassName}
  126. title={formatMessage(collapsed ? messages.show : messages.hide)}
  127. aria-label={formatMessage(collapsed ? messages.show : messages.hide)}
  128. aria-pressed={collapsed ? 'false' : 'true'}
  129. onClick={this.handleToggleClick}
  130. >
  131. <i className='icon-with-badge'>
  132. <Icon id='sliders' />
  133. {collapseIssues && <i className='icon-with-badge__issue-badge' />}
  134. </i>
  135. </button>
  136. );
  137. }
  138. const hasTitle = icon && title;
  139. const component = (
  140. <div className={wrapperClassName}>
  141. <h1 className={buttonClassName}>
  142. {hasTitle && (
  143. <button onClick={this.handleTitleClick}>
  144. <Icon id={icon} fixedWidth className='column-header__icon' />
  145. {title}
  146. </button>
  147. )}
  148. {!hasTitle && backButton}
  149. <div className='column-header__buttons'>
  150. {hasTitle && backButton}
  151. {extraButton}
  152. {collapseButton}
  153. </div>
  154. </h1>
  155. <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
  156. <div className='column-header__collapsible-inner'>
  157. {(!collapsed || animating) && collapsedContent}
  158. </div>
  159. </div>
  160. {appendContent}
  161. </div>
  162. );
  163. if (multiColumn || placeholder) {
  164. return component;
  165. } else {
  166. // The portal container and the component may be rendered to the DOM in
  167. // the same React render pass, so the container might not be available at
  168. // the time `render()` is called.
  169. const container = document.getElementById('tabs-bar__portal');
  170. if (container === null) {
  171. // The container wasn't available, force a re-render so that the
  172. // component can eventually be inserted in the container and not scroll
  173. // with the rest of the area.
  174. this.forceUpdate();
  175. return component;
  176. } else {
  177. return createPortal(component, container);
  178. }
  179. }
  180. }
  181. }