import { Utils, localized } from 'mailspring-exports'; import React, { Component, CSSProperties } from 'react'; import { DropZone } from './drop-zone'; import { RetinaImg } from './retina-img'; import OutlineViewItem from './outline-view-item'; import PropTypes from 'prop-types'; export interface IOutlineViewItem { id?: string; title?: string; iconName?: string; name?: string; children?: IOutlineViewItem[]; contextMenuLabel?: string; collapsed?: boolean; className?: string; count?: number; counterStyle?: string; inputPlaceholder?: string; editing?: boolean; selected?: boolean; onItemCreated?: (...args: any[]) => any; onCollapseToggled?: (...args: any[]) => any; shouldAcceptDrop?: (...args: any[]) => any; onInputCleared?: (...args: any[]) => any; onDrop?: (...args: any[]) => any; onSelect?: (...args: any[]) => any; onDelete?: (...args: any[]) => any; onEdited?: (...args: any[]) => any; onExport?: (...args: any[]) => any; } interface OutlineViewProps { title: string; items: IOutlineViewItem[]; iconName?: string; collapsed?: boolean; titleColor?: string; onCollapseToggled?: (props: OutlineViewProps) => void; onItemCreated?: (displayName) => void; } interface OutlineViewState { showCreateInput: boolean; } /* * Renders a section that contains a list of {@link OutlineViewItem}s. These items can * be arbitrarily nested. See docs for {@link OutlineViewItem}. * An OutlineView behaves like a controlled React component, with callbacks for * collapsing and creating items, and respective props for their value. Is it up * to the parent component to determine the state of the OutlineView. * * This component resembles OS X's default OutlineView or Sourcelist * * OutlineView supports: * - Collapsing and uncollapsing * - Adding new items to the outline view * * @param {object} props - props for OutlineView * @param {string} props.title - Title to display * @param {string} props.iconName - Icon name to use when displaying input to * add a new item. See {@link RetinaImg} for further reference. * @param {array} props.items - Array of strings or numbers to display as {@link * OutlineViewItem}s * @param {boolean} props.collapsed - Whether the OutlineView is collapsed or * not * @param {string} props.titleColor - Colored bar that is displayed to highlight the title * @param {props.onItemCreated} props.onItemCreated * @param {props.onCollapseToggled} props.onCollapseToggled * @class OutlineView */ export class OutlineView extends Component { static displayName = 'OutlineView'; /* * If provided, this function will be called when an item has been created. * @callback props.onItemCreated * @param {string} value - The value for the created item */ /* * If provided, this function will be called when the user clicks the action * to collapse or uncollapse the OutlineView * @callback props.onCollapseToggled * @param {object} props - The entire props object for this OutlineView */ static propTypes = { title: PropTypes.string, titleColor: PropTypes.string, iconName: PropTypes.string, items: PropTypes.array, collapsed: PropTypes.bool, onItemCreated: PropTypes.func, onCollapseToggled: PropTypes.func, }; static defaultProps = { title: '', items: [], }; state = { showCreateInput: false, }; _clickingCreateButton: boolean; _expandTimeout?: NodeJS.Timer; shouldComponentUpdate(nextProps, nextState) { return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state); } componentWillUnmount() { clearTimeout(this._expandTimeout); } // Handlers _onCreateButtonMouseDown = () => { this._clickingCreateButton = true; }; _onCreateButtonClicked = () => { this._clickingCreateButton = false; this.setState({ showCreateInput: !this.state.showCreateInput }); }; _onCollapseToggled = () => { if (this.props.onCollapseToggled) { this.props.onCollapseToggled(this.props); } }; _onDragStateChange = ({ isDropping }) => { if (this.props.collapsed && !this._expandTimeout && isDropping) { this._expandTimeout = setTimeout(this._onCollapseToggled, 650); } else if (this._expandTimeout && !isDropping) { clearTimeout(this._expandTimeout); this._expandTimeout = null; } }; _onItemCreated = (item, value) => { this.setState({ showCreateInput: false }); this.props.onItemCreated(value); }; _onCreateInputCleared = () => { if (!this._clickingCreateButton) { this.setState({ showCreateInput: false }); } }; // Renderers _renderCreateInput(props = this.props) { const item = { id: `add-item-${props.title}`, name: '', children: [], editing: true, iconName: props.iconName, onEdited: this._onItemCreated, inputPlaceholder: localized('Create new item'), onInputCleared: this._onCreateInputCleared, }; return ; } _renderCreateButton() { return ( ); } _renderHeading(allowCreate, collapsed, collapsible) { const collapseLabel = collapsed ? localized('Show') : localized('Hide'); let style: CSSProperties = {}; if (this.props.titleColor) { style = { height: '50%', paddingLeft: '4px', borderLeftWidth: '4px', borderLeftColor: this.props.titleColor, borderLeftStyle: 'solid', }; } return ( true} onDragStateChange={this._onDragStateChange} shouldAcceptDrop={() => true} > {this.props.title} {allowCreate ? this._renderCreateButton() : null} {collapsible ? ( {collapseLabel} ) : null} ); } _renderItems() { return this.props.items.map(item => ); } _renderOutline(allowCreate, collapsed) { if (collapsed) { return ; } const showInput = allowCreate && this.state.showCreateInput; return (
{showInput ? this._renderCreateInput() : null} {this._renderItems()}
); } render() { const collapsible = this.props.onCollapseToggled; const collapsed = this.props.collapsed; const allowCreate = this.props.onItemCreated != null && !collapsed; return (
{this._renderHeading(allowCreate, collapsed, collapsible)} {this._renderOutline(allowCreate, collapsed)}
); } }