/** * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal */ import * as React from 'react'; import { Color } from '../../mol-util/color'; import { Icon, ArrowRightSvg, ArrowDropDownSvg, RemoveSvg, AddSvg } from './icons'; export type ColorAccent = 'cyan' | 'red' | 'gray' | 'green' | 'purple' | 'blue' | 'orange' export class ControlGroup extends React.Component<{ header: string, title?: string, initialExpanded?: boolean, hideExpander?: boolean, hideOffset?: boolean, topRightIcon?: React.FC, headerLeftMargin?: string, onHeaderClick?: () => void, noTopMargin?: boolean, childrenClassName?: string, maxHeight?: string }, { isExpanded: boolean }> { state = { isExpanded: !!this.props.initialExpanded } headerClicked = () => { if (this.props.onHeaderClick) { this.props.onHeaderClick(); } else { this.setState({ isExpanded: !this.state.isExpanded }); } } render() { let groupClassName = this.props.hideOffset ? 'msp-control-group-children' : 'msp-control-group-children msp-control-offset'; if (this.props.childrenClassName) groupClassName += ' ' + this.props.childrenClassName; // TODO: customize header style (bg color, togle button etc) return
{this.state.isExpanded &&
{this.props.children}
}
; } } export interface TextInputProps { className?: string, style?: React.CSSProperties, value: T, fromValue?(v: T): string, toValue?(s: string): T, // TODO: add error/help messages here? isValid?(s: string): boolean, onChange(value: T): void, onEnter?(): void, onBlur?(): void, delayMs?: number, blurOnEnter?: boolean, blurOnEscape?: boolean, isDisabled?: boolean, placeholder?: string, numeric?: boolean } interface TextInputState { originalValue: string, value: string } function _id(x: any) { return x; } export class TextInput extends React.PureComponent, TextInputState> { private input = React.createRef(); private delayHandle: any = void 0; private pendingValue: T | undefined = void 0; state = { originalValue: '', value: '' } onBlur = () => { this.setState({ value: '' + this.state.originalValue }); if (this.props.onBlur) this.props.onBlur(); } get isPending() { return typeof this.delayHandle !== 'undefined'; } clearTimeout() { if (this.isPending) { clearTimeout(this.delayHandle); this.delayHandle = void 0; } } raiseOnChange = () => { if (this.pendingValue === void 0) return; this.props.onChange(this.pendingValue!); this.pendingValue = void 0; } triggerChanged(formatted: string, converted: T) { this.clearTimeout(); if (formatted === this.state.originalValue) return; if (this.props.delayMs) { this.pendingValue = converted; this.delayHandle = setTimeout(this.raiseOnChange, this.props.delayMs); } else { this.props.onChange(converted); } } onChange = (e: React.ChangeEvent) => { const value = e.target.value; let isInvalid = (this.props.isValid && !this.props.isValid(value)) || (this.props.numeric && Number.isNaN(+value)); if (isInvalid) { this.clearTimeout(); this.setState({ value }); return; } if (this.props.numeric) { this.setState({ value }, () => this.triggerChanged(value, +value as any)); } else { const converted = (this.props.toValue || _id)(value); const formatted = (this.props.fromValue || _id)(converted); this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted)); } } onKeyUp = (e: React.KeyboardEvent) => { if (e.charCode === 27 || e.keyCode === 27 || e.key === 'Escape') { if (this.props.blurOnEscape && this.input.current) { this.input.current.blur(); } } } onKeyPress = (e: React.KeyboardEvent) => { if (e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter') { if (this.isPending) { this.clearTimeout(); this.raiseOnChange(); } if (this.props.blurOnEnter && this.input.current) { this.input.current.blur(); } if (this.props.onEnter) this.props.onEnter(); } e.stopPropagation(); } static getDerivedStateFromProps(props: TextInputProps, state: TextInputState) { const value = props.fromValue ? props.fromValue(props.value) : props.value; if (value === state.originalValue) return null; return { originalValue: value, value }; } render() { return ; } } export class ExpandableControlRow extends React.Component<{ label: string, colorStripe?: Color, pivot: JSX.Element, controls: JSX.Element }, { isExpanded: boolean }> { state = { isExpanded: false }; toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); render() { const { label, pivot, controls } = this.props; // TODO: fix the inline CSS return <> {label} } control={pivot}> {this.props.colorStripe &&
} {this.state.isExpanded &&
{controls}
} ; } } export function SectionHeader(props: { icon?: React.FC, title: string | JSX.Element, desc?: string, accent?: ColorAccent }) { return
{props.icon && } {props.title} {props.desc}
; } export type ButtonProps = { style?: React.CSSProperties, className?: string, disabled?: boolean, title?: string, icon?: React.FC, commit?: boolean | 'on' | 'off' children?: React.ReactNode, onClick?: (e: React.MouseEvent) => void, onContextMenu?: (e: React.MouseEvent) => void, onMouseEnter?: (e: React.MouseEvent) => void, onMouseLeave?: (e: React.MouseEvent) => void, inline?: boolean, 'data-id'?: string, 'data-color'?: Color, flex?: boolean | string | number, noOverflow?: boolean } export function Button(props: ButtonProps) { let className = 'msp-btn'; if (!props.inline) className += ' msp-btn-block'; if (props.noOverflow) className += ' msp-no-overflow'; if (props.flex) className += ' msp-flex-item'; if (props.commit === 'on' || props.commit) className += ' msp-btn-commit msp-btn-commit-on'; if (props.commit === 'off') className += ' msp-btn-commit msp-btn-commit-off'; if (!props.children) className += ' msp-btn-childless'; if (props.className) className += ' ' + props.className; let style: React.CSSProperties | undefined = void 0; if (props.flex) { if (typeof props.flex === 'number') style = { flex: `0 0 ${props.flex}px`, padding: 0, maxWidth: `${props.flex}px` }; else if (typeof props.flex === 'string') style = { flex: `0 0 ${props.flex}`, padding: 0, maxWidth: props.flex }; } if (props.style) { if (style) Object.assign(style, props.style); else style = props.style; } return ; } export function IconButton(props: { svg?: React.FC, small?: boolean, onClick: (e: React.MouseEvent) => void, title?: string, toggleState?: boolean, disabled?: boolean, className?: string, style?: React.CSSProperties, 'data-id'?: string, extraContent?: JSX.Element, flex?: boolean | string | number, transparent?: boolean }) { let className = `msp-btn msp-btn-icon${props.small ? '-small' : ''}${props.className ? ' ' + props.className : ''}`; if (typeof props.toggleState !== 'undefined') { className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`; } if (props.transparent) { className += ' msp-transparent-bg'; } let style: React.CSSProperties | undefined = void 0; if (props.flex) { if (typeof props.flex === 'boolean') style = { flex: '0 0 32px', padding: 0 }; else if (typeof props.flex === 'number') style = { flex: `0 0 ${props.flex}px`, padding: 0, maxWidth: `${props.flex}px` }; else style = { flex: `0 0 ${props.flex}`, padding: 0, maxWidth: props.flex }; } if (props.style) { if (style) Object.assign(style, props.style); else style = props.style; } return ; } export type ToggleButtonProps = { style?: React.CSSProperties, inline?: boolean, className?: string, disabled?: boolean, label?: string | JSX.Element, title?: string, icon?: React.FC, isSelected?: boolean, toggle: () => void } export class ToggleButton extends React.PureComponent { onClick = (e: React.MouseEvent) => { e.currentTarget.blur(); this.props.toggle(); } render() { const props = this.props; const label = props.label; const className = props.isSelected ? `${props.className || ''} msp-control-current` : props.className; return ; } } export class ExpandGroup extends React.PureComponent<{ header: string, headerStyle?: React.CSSProperties, initiallyExpanded?: boolean, accent?: boolean, noOffset?: boolean, marginTop?: 0 | string, headerLeftMargin?: string }, { isExpanded: boolean }> { state = { isExpanded: !!this.props.initiallyExpanded }; toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); render() { return <>
{this.state.isExpanded && (this.props.noOffset ? this.props.children :
{this.props.children}
)} ; } } export type ControlRowProps = { title?: string, label?: React.ReactNode, control?: React.ReactNode, className?: string, children?: React.ReactNode } export function ControlRow(props: ControlRowProps) { let className = 'msp-control-row'; if (props.className) className += ' ' + props.className; return
{props.label}
{props.control}
{props.children}
; }