import * as React from "react"; import { Input as BSInput, InputProps } from "reactstrap"; import { isArray, isFunction } from "util"; import { MathInput } from "./MathInput"; import { IInternalPromptProps } from "./Prompt"; import { IInputProps, InputPromptData } from "./QuizData"; const INDEX = 0; export enum InputType { String = "string", Number = "number", Math = "math", Custom = "custom" } export type IInputAnswerHandler = (input: string) => boolean; export type IInputAnswer = | string | number | string[] | number[] | IInputAnswerHandler; export interface IInputPromptProps { children: ( Input: React.ComponentType ) => React.ReactChild | React.ReactChild[]; } export class InputPrompt extends React.PureComponent { static is(value: any): value is InputPrompt { return React.isValidElement(value) && value.type === InputPrompt; } render() { return null; } } export interface IInternalInputPromptProps extends IInternalPromptProps { prompt: InputPromptData; input: string[]; update(fn: (input: string[]) => string[]): string[]; } interface IInputState { value: string; } export class InternalInputPrompt extends React.PureComponent< IInternalInputPromptProps > { private static INPUT_STYLE = { width: "auto", display: "inline-block" }; private Input: React.ComponentType; private index: number = 0; constructor(props: IInternalInputPromptProps) { super(props); this.Input = this.createInput(); } createOnChange = (index: number) => (e: any) => { this.props.update(input => { const nextInput = input.slice(); nextInput[index] = e.target.value; return nextInput; }); }; componentDidUpdate(props: IInternalInputPromptProps) { if (props.prompt !== this.props.prompt) { this.index = 0; } } render() { return this.props.prompt.render(this.Input); } private createInput() { const _this = this; return class Input extends React.Component { static defaultProps = { Component: BSInput }; private index: number; private Component: React.ComponentType; constructor(props: IInputProps) { super(props); this.index = _this.index++; this.state = { value: _this.props.input[this.index] || "" }; this.Component = this.props.type === InputType.Math ? MathInput : (this.props.Component as any); _this.props.prompt.addAnswer(this.index, this.parseAnswer()); } shouldComponentUpdate(props: IInputProps) { return this.state.value !== _this.props.input[this.index]; } componentWillReceiveProps(props: IInputProps) { this.setState({ value: _this.props.input[this.index] || "" }); } parseAnswer(): IInputAnswerHandler { switch (this.props.type) { case InputType.String: return createAnswer(this.props.answer, createStringAnswer); case InputType.Number: return createAnswer(this.props.answer, createNumberAnswer); case InputType.Math: return createAnswer(this.props.answer, createMathAnswer); case InputType.Custom: if (isFunction(this.props.answer)) { return this.props.answer as IInputAnswerHandler; } else { throw new TypeError("Custom type inputs must be functions"); } } } render() { const Component = this.Component as any; return ( ); } }; } } const createAnswer = ( answer: any, createFn: (input: any) => IInputAnswerHandler ) => { if (isArray(answer)) { const answers = answer.map(a => createFn(a)); return (input: any) => answers.some(fn => fn(input)); } else { return createFn(answer); } }; const createStringAnswer = (answer: string) => (input: string) => { return input === answer; }; const createNumberAnswer = (answer: number) => (input: string) => { return parseFloat(input) === answer; }; const createMathAnswer = (answer: string) => (input: string) => { return input === answer; };