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.

221 lines
7.0 KiB

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