166 lines
6.1 KiB
TypeScript
166 lines
6.1 KiB
TypeScript
import { ObjectMap } from '@0x/types';
|
|
import * as _ from 'lodash';
|
|
import * as React from 'react';
|
|
|
|
import { ColorOption } from '../style/theme';
|
|
import { util } from '../util/util';
|
|
|
|
import { Input } from './ui/input';
|
|
|
|
export enum ScalingInputPhase {
|
|
FixedFontSize,
|
|
ScalingFontSize,
|
|
}
|
|
|
|
export interface ScalingSettings {
|
|
percentageToReduceFontSizePerCharacter: number;
|
|
// 1ch = the width of the 0 chararacter.
|
|
// Allow to customize 'char' length for different characters.
|
|
characterWidthOverrides: ObjectMap<number>;
|
|
// How much room to leave to the right of the scaling input.
|
|
additionalInputSpaceInCh: number;
|
|
}
|
|
|
|
export interface ScalingInputProps {
|
|
type?: string;
|
|
textLengthThreshold: number;
|
|
maxFontSizePx: number;
|
|
value: string;
|
|
emptyInputWidthCh: number;
|
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
onFontSizeChange: (fontSizePx: number) => void;
|
|
fontColor?: ColorOption;
|
|
placeholder?: string;
|
|
maxLength?: number;
|
|
scalingSettings: ScalingSettings;
|
|
isDisabled: boolean;
|
|
hasAutofocus: boolean;
|
|
}
|
|
|
|
export interface ScalingInputSnapshot {
|
|
inputWidthPx: number;
|
|
}
|
|
|
|
// These are magic numbers that were determined experimentally.
|
|
const defaultScalingSettings: ScalingSettings = {
|
|
percentageToReduceFontSizePerCharacter: 0.1,
|
|
characterWidthOverrides: {
|
|
'1': 0.7,
|
|
'.': 0.4,
|
|
},
|
|
additionalInputSpaceInCh: 0.4,
|
|
};
|
|
|
|
export class ScalingInput extends React.PureComponent<ScalingInputProps> {
|
|
public static defaultProps = {
|
|
onChange: util.boundNoop,
|
|
onFontSizeChange: util.boundNoop,
|
|
maxLength: 9,
|
|
scalingSettings: defaultScalingSettings,
|
|
isDisabled: false,
|
|
hasAutofocus: false,
|
|
};
|
|
private readonly _inputRef = React.createRef<HTMLInputElement>();
|
|
public static getPhase(textLengthThreshold: number, value: string): ScalingInputPhase {
|
|
if (value.length <= textLengthThreshold) {
|
|
return ScalingInputPhase.FixedFontSize;
|
|
}
|
|
return ScalingInputPhase.ScalingFontSize;
|
|
}
|
|
public static getPhaseFromProps(props: ScalingInputProps): ScalingInputPhase {
|
|
const { value, textLengthThreshold } = props;
|
|
return ScalingInput.getPhase(textLengthThreshold, value);
|
|
}
|
|
public static calculateFontSize(
|
|
textLengthThreshold: number,
|
|
maxFontSizePx: number,
|
|
phase: ScalingInputPhase,
|
|
value: string,
|
|
percentageToReduceFontSizePerCharacter: number,
|
|
): number {
|
|
if (phase !== ScalingInputPhase.ScalingFontSize) {
|
|
return maxFontSizePx;
|
|
}
|
|
const charactersOverMax = value.length - textLengthThreshold;
|
|
const scalingFactor = (1 - percentageToReduceFontSizePerCharacter) ** charactersOverMax;
|
|
const fontSize = scalingFactor * maxFontSizePx;
|
|
return fontSize;
|
|
}
|
|
public static calculateFontSizeFromProps(props: ScalingInputProps, phase: ScalingInputPhase): number {
|
|
const { textLengthThreshold, value, maxFontSizePx, scalingSettings } = props;
|
|
return ScalingInput.calculateFontSize(
|
|
textLengthThreshold,
|
|
maxFontSizePx,
|
|
phase,
|
|
value,
|
|
scalingSettings.percentageToReduceFontSizePerCharacter,
|
|
);
|
|
}
|
|
public componentDidMount(): void {
|
|
// Trigger an initial notification of the calculated fontSize.
|
|
const currentPhase = ScalingInput.getPhaseFromProps(this.props);
|
|
const currentFontSize = ScalingInput.calculateFontSizeFromProps(this.props, currentPhase);
|
|
this.props.onFontSizeChange(currentFontSize);
|
|
}
|
|
public componentDidUpdate(prevProps: ScalingInputProps): void {
|
|
const prevPhase = ScalingInput.getPhaseFromProps(prevProps);
|
|
const curPhase = ScalingInput.getPhaseFromProps(this.props);
|
|
const prevFontSize = ScalingInput.calculateFontSizeFromProps(prevProps, prevPhase);
|
|
const curFontSize = ScalingInput.calculateFontSizeFromProps(this.props, curPhase);
|
|
// If font size has changed, notify.
|
|
if (prevFontSize !== curFontSize) {
|
|
this.props.onFontSizeChange(curFontSize);
|
|
}
|
|
}
|
|
public render(): React.ReactNode {
|
|
const { type, hasAutofocus, isDisabled, fontColor, placeholder, value, maxLength } = this.props;
|
|
const phase = ScalingInput.getPhaseFromProps(this.props);
|
|
return (
|
|
<Input
|
|
type={type}
|
|
ref={this._inputRef as any}
|
|
fontColor={fontColor}
|
|
onChange={this._handleChange}
|
|
value={value}
|
|
placeholder={placeholder}
|
|
fontSize={`${this._calculateFontSize(phase)}px`}
|
|
width={this._calculateWidth(phase)}
|
|
maxLength={maxLength}
|
|
disabled={isDisabled}
|
|
autoFocus={hasAutofocus}
|
|
/>
|
|
);
|
|
}
|
|
private readonly _calculateWidth = (phase: ScalingInputPhase): string => {
|
|
const { value, scalingSettings } = this.props;
|
|
if (_.isEmpty(value)) {
|
|
return `${this.props.emptyInputWidthCh}ch`;
|
|
}
|
|
const lengthInCh = _.reduce(
|
|
value.split(''),
|
|
(sum, char) => {
|
|
const widthOverride = scalingSettings.characterWidthOverrides[char];
|
|
if (!_.isUndefined(widthOverride)) {
|
|
// tslint is confused
|
|
// tslint:disable-next-line:restrict-plus-operands
|
|
return sum + widthOverride;
|
|
}
|
|
return sum + 1;
|
|
},
|
|
scalingSettings.additionalInputSpaceInCh,
|
|
);
|
|
return `${lengthInCh}ch`;
|
|
};
|
|
private readonly _calculateFontSize = (phase: ScalingInputPhase): number => {
|
|
return ScalingInput.calculateFontSizeFromProps(this.props, phase);
|
|
};
|
|
private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
const value = event.target.value;
|
|
const { maxLength } = this.props;
|
|
if (!_.isUndefined(value) && !_.isUndefined(maxLength) && value.length > maxLength) {
|
|
return;
|
|
}
|
|
this.props.onChange(event);
|
|
};
|
|
}
|