229 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
//  Package imports.
 | 
						|
import classNames from 'classnames';
 | 
						|
import PropTypes from 'prop-types';
 | 
						|
import React from 'react';
 | 
						|
import Overlay from 'react-overlays/lib/Overlay';
 | 
						|
 | 
						|
//  Components.
 | 
						|
import IconButton from 'flavours/glitch/components/icon_button';
 | 
						|
import ComposerOptionsDropdownContent from './content';
 | 
						|
 | 
						|
//  Utils.
 | 
						|
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 | 
						|
import { assignHandlers } from 'flavours/glitch/util/react_helpers';
 | 
						|
 | 
						|
//  Handlers.
 | 
						|
const handlers = {
 | 
						|
 | 
						|
  //  Closes the dropdown.
 | 
						|
  handleClose () {
 | 
						|
    this.setState({ open: false });
 | 
						|
  },
 | 
						|
 | 
						|
  //  The enter key toggles the dropdown's open state, and the escape
 | 
						|
  //  key closes it.
 | 
						|
  handleKeyDown ({ key }) {
 | 
						|
    const {
 | 
						|
      handleClose,
 | 
						|
      handleToggle,
 | 
						|
    } = this.handlers;
 | 
						|
    switch (key) {
 | 
						|
    case 'Enter':
 | 
						|
      handleToggle(key);
 | 
						|
      break;
 | 
						|
    case 'Escape':
 | 
						|
      handleClose();
 | 
						|
      break;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  //  Creates an action modal object.
 | 
						|
  handleMakeModal () {
 | 
						|
    const component = this;
 | 
						|
    const {
 | 
						|
      items,
 | 
						|
      onChange,
 | 
						|
      onModalOpen,
 | 
						|
      onModalClose,
 | 
						|
      value,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    //  Required props.
 | 
						|
    if (!(onChange && onModalOpen && onModalClose && items)) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    //  The object.
 | 
						|
    return {
 | 
						|
      actions: items.map(
 | 
						|
        ({
 | 
						|
          name,
 | 
						|
          ...rest
 | 
						|
        }) => ({
 | 
						|
          ...rest,
 | 
						|
          active: value && name === value,
 | 
						|
          name,
 | 
						|
          onClick (e) {
 | 
						|
            e.preventDefault();  //  Prevents focus from changing
 | 
						|
            onModalClose();
 | 
						|
            onChange(name);
 | 
						|
          },
 | 
						|
          onPassiveClick (e) {
 | 
						|
            e.preventDefault();  //  Prevents focus from changing
 | 
						|
            onChange(name);
 | 
						|
            component.setState({ needsModalUpdate: true });
 | 
						|
          },
 | 
						|
        })
 | 
						|
      ),
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  //  Toggles opening and closing the dropdown.
 | 
						|
  handleToggle ({ target }) {
 | 
						|
    const { handleMakeModal } = this.handlers;
 | 
						|
    const { onModalOpen } = this.props;
 | 
						|
    const { open } = this.state;
 | 
						|
 | 
						|
    //  If this is a touch device, we open a modal instead of the
 | 
						|
    //  dropdown.
 | 
						|
    if (isUserTouching()) {
 | 
						|
 | 
						|
      //  This gets the modal to open.
 | 
						|
      const modal = handleMakeModal();
 | 
						|
 | 
						|
      //  If we can, we then open the modal.
 | 
						|
      if (modal && onModalOpen) {
 | 
						|
        onModalOpen(modal);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const { top } = target.getBoundingClientRect();
 | 
						|
    this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
 | 
						|
    //  Otherwise, we just set our state to open.
 | 
						|
    this.setState({ open: !open });
 | 
						|
  },
 | 
						|
 | 
						|
  //  If our modal is open and our props update, we need to also update
 | 
						|
  //  the modal.
 | 
						|
  handleUpdate () {
 | 
						|
    const { handleMakeModal } = this.handlers;
 | 
						|
    const { onModalOpen } = this.props;
 | 
						|
    const { needsModalUpdate } = this.state;
 | 
						|
 | 
						|
    //  Gets our modal object.
 | 
						|
    const modal = handleMakeModal();
 | 
						|
 | 
						|
    //  Reopens the modal with the new object.
 | 
						|
    if (needsModalUpdate && modal && onModalOpen) {
 | 
						|
      onModalOpen(modal);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
//  The component.
 | 
						|
export default class ComposerOptionsDropdown extends React.PureComponent {
 | 
						|
 | 
						|
  //  Constructor.
 | 
						|
  constructor (props) {
 | 
						|
    super(props);
 | 
						|
    assignHandlers(this, handlers);
 | 
						|
    this.state = {
 | 
						|
      needsModalUpdate: false,
 | 
						|
      open: false,
 | 
						|
      placement: null,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  //  Updates our modal as necessary.
 | 
						|
  componentDidUpdate (prevProps) {
 | 
						|
    const { handleUpdate } = this.handlers;
 | 
						|
    const { items } = this.props;
 | 
						|
    const { needsModalUpdate } = this.state;
 | 
						|
    if (needsModalUpdate && items.find(
 | 
						|
      (item, i) => item.on !== prevProps.items[i].on
 | 
						|
    )) {
 | 
						|
      handleUpdate();
 | 
						|
      this.setState({ needsModalUpdate: false });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  //  Rendering.
 | 
						|
  render () {
 | 
						|
    const {
 | 
						|
      handleClose,
 | 
						|
      handleKeyDown,
 | 
						|
      handleToggle,
 | 
						|
    } = this.handlers;
 | 
						|
    const {
 | 
						|
      active,
 | 
						|
      disabled,
 | 
						|
      title,
 | 
						|
      icon,
 | 
						|
      items,
 | 
						|
      onChange,
 | 
						|
      value,
 | 
						|
    } = this.props;
 | 
						|
    const { open, placement } = this.state;
 | 
						|
    const computedClass = classNames('composer--options--dropdown', {
 | 
						|
      active,
 | 
						|
      open,
 | 
						|
      top: placement === 'top',
 | 
						|
    });
 | 
						|
 | 
						|
    //  The result.
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        className={computedClass}
 | 
						|
        onKeyDown={handleKeyDown}
 | 
						|
      >
 | 
						|
        <IconButton
 | 
						|
          active={open || active}
 | 
						|
          className='value'
 | 
						|
          disabled={disabled}
 | 
						|
          icon={icon}
 | 
						|
          onClick={handleToggle}
 | 
						|
          size={18}
 | 
						|
          style={{
 | 
						|
            height: null,
 | 
						|
            lineHeight: '27px',
 | 
						|
          }}
 | 
						|
          title={title}
 | 
						|
        />
 | 
						|
        <Overlay
 | 
						|
          containerPadding={20}
 | 
						|
          placement={placement}
 | 
						|
          show={open}
 | 
						|
          target={this}
 | 
						|
        >
 | 
						|
          <ComposerOptionsDropdownContent
 | 
						|
            items={items}
 | 
						|
            onChange={onChange}
 | 
						|
            onClose={handleClose}
 | 
						|
            value={value}
 | 
						|
          />
 | 
						|
        </Overlay>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
//  Props.
 | 
						|
ComposerOptionsDropdown.propTypes = {
 | 
						|
  active: PropTypes.bool,
 | 
						|
  disabled: PropTypes.bool,
 | 
						|
  icon: PropTypes.string,
 | 
						|
  items: PropTypes.arrayOf(PropTypes.shape({
 | 
						|
    icon: PropTypes.string,
 | 
						|
    meta: PropTypes.node,
 | 
						|
    name: PropTypes.string.isRequired,
 | 
						|
    on: PropTypes.bool,
 | 
						|
    text: PropTypes.node,
 | 
						|
  })).isRequired,
 | 
						|
  onChange: PropTypes.func,
 | 
						|
  onModalClose: PropTypes.func,
 | 
						|
  onModalOpen: PropTypes.func,
 | 
						|
  title: PropTypes.string,
 | 
						|
  value: PropTypes.string,
 | 
						|
};
 |