import { DEFAULT_RNG, ParkMillerRng, Rng } from "@stembord/rand"; import * as React from "react"; import { InputProps } from "reactstrap"; import { isArray, isBoolean, isFunction, isString } from "util"; import { Description } from "./Description"; import { Explanation } from "./Explanation"; import { IInputAnswer, IInputAnswerHandler, IInputPromptProps, InputPrompt, InputType } from "./InputPrompt"; import { IMultipleChoicePromptProps, MultipleChoicePrompt } from "./MultipleChoicePrompt"; import { Prompt } from "./Prompt"; import { IQuestionProps, Question } from "./Question"; import { IQuizProps } from "./Quiz"; import { ITrueFalsePromptProps, TrueFalsePrompt } from "./TrueFalsePrompt"; import { toArray } from "./utils"; export abstract class PromptData { static parse(quiz: QuizData, child: React.ReactElement) { if (InputPrompt.is(child)) { return InputPromptData.parse(quiz, child); } else if (MultipleChoicePrompt.is(child)) { return MultipleChoicePromptData.parse(quiz, child); } else if (TrueFalsePrompt.is(child)) { return TrueFalsePromptData.parse(quiz, child); } else { throw new TypeError("Invalid Prompt " + child); } } quiz: QuizData; prompt?: React.ReactChild | React.ReactChild[]; constructor(quiz: QuizData, prompt?: React.ReactChild | React.ReactChild[]) { this.quiz = quiz; this.prompt = prompt; } abstract getScore(input: any[]): number; } export class TrueFalsePromptData extends PromptData { static parse( quiz: QuizData, child: React.ReactElement ) { return new TrueFalsePromptData( quiz, child.props.children, child.props.answer, child.props.true, child.props.false ); } static is(value: any): value is TrueFalsePromptData { return ( value && isBoolean(value.answer) && (isString(value.true) || React.isValidElement(value.true)) && (isString(value.false) || React.isValidElement(value.false)) ); } answer: boolean; true: React.ReactChild = "True"; false: React.ReactChild = "False"; constructor( quiz: QuizData, prompt: React.ReactChild | React.ReactChild[], answer: boolean, trueValue?: React.ReactChild, falseValue?: React.ReactChild ) { super(quiz, prompt); this.answer = answer; if (trueValue) { this.true = trueValue; } if (falseValue) { this.false = falseValue; } } getScore(input: [boolean]): number { return !!this.answer === !!input[0] ? 1 : 0; } } export interface IChoiceData { answer: boolean; children: React.ReactChild | React.ReactChild[]; } export class MultipleChoicePromptData extends PromptData { static parse( quiz: QuizData, child: React.ReactElement ) { return new MultipleChoicePromptData( quiz, child.props.children, child.props.choices.map(child => ({ answer: child.props.answer === true, children: child.props.children })), child.props.onlyOne, child.props.randomOrder ); } static is(value: any): value is TrueFalsePromptData { return ( value && isBoolean(value.onlyOne) && isBoolean(value.randomOrder) && isArray(value.choices) ); } onlyOne: boolean = false; randomOrder: boolean = true; choices: IChoiceData[] = []; constructor( quiz: QuizData, prompt: React.ReactChild | React.ReactChild[], choices: IChoiceData[], onlyOne?: boolean, randomOrder?: boolean ) { super(quiz, prompt); this.choices = choices; this.onlyOne = onlyOne != null ? !!onlyOne : false; this.randomOrder = randomOrder != null ? !!randomOrder : false; if (this.randomOrder) { this.sortChoices(); } } getScore(input: boolean[]): number { if (this.onlyOne) { return input.some((input, index) => this.choices.some( (choice, choiceIndex) => choice.answer && choiceIndex === index ) ) ? 1 : 0; } else { return this.choices.reduce( (count, choice, index) => count + (choice.answer && input[index] === true ? 1 : 0), 0 ); } } private sortChoices() { this.choices.sort((a, b) => (this.quiz.rng.nextFloat() < 0.5 ? -1 : 1)); } } export interface IInputProps { Component?: React.ComponentType; inputProps?: InputProps; type: InputType; answer: IInputAnswer; placeholder?: string; } export type Input = React.ComponentType; export class InputPromptData extends PromptData { static parse(quiz: QuizData, child: React.ReactElement) { return new InputPromptData(quiz, child.props.children); } static is(value: any): value is TrueFalsePromptData { return value && isArray(value.answers) && isFunction(value.render); } answers: IInputAnswerHandler[] = []; render: (Input: Input) => React.ReactChild | React.ReactChild[]; constructor( quiz: QuizData, render: (Input: Input) => React.ReactChild | React.ReactChild[] ) { super(quiz); this.render = render; } addAnswer(index: number, answer: IInputAnswerHandler) { this.answers[index] = answer; } getScore(input: string[]): number { return ( this.answers.reduce( (count, answer, index) => count + (answer(input[index]) ? 1 : 0), 0 ) / this.answers.length ); } } export class ExplainationData { static parse(quiz: QuizData, child: React.ReactElement) { return new ExplainationData( quiz, child.props.multipleSteps === true, child.props.children ); } quiz: QuizData; multipleSteps: boolean; children: React.ReactChild[]; constructor( quiz: QuizData, multipleSteps: boolean, children: React.ReactChild | React.ReactChild[] ) { this.quiz = quiz; this.multipleSteps = multipleSteps; this.children = toArray(children); } } export class QuestionData { static parse(quiz: QuizData, child: React.ReactElement) { const childArray = toArray(child.props.children), prompt = childArray.find(Prompt.is), explaination = childArray.find(Explanation.is); if (!prompt) { throw new TypeError("Question requires a Prompt"); } return new QuestionData( quiz, PromptData.parse(quiz, prompt), explaination ? ExplainationData.parse(quiz, explaination) : undefined ); } quiz: QuizData; prompt: PromptData; explaination?: ExplainationData; constructor( quiz: QuizData, prompt: PromptData, explaination?: ExplainationData ) { this.quiz = quiz; this.prompt = prompt; this.explaination = explaination; } } export class QuizData { static parse(props: IQuizProps) { const quiz = new QuizData(props.title); if (props.seed) { quiz.seed = props.seed; } if (props.autoStart) { quiz.autoStart = !!props.autoStart; } quiz.randomOrder = props.randomOrder != null ? !!props.randomOrder : false; quiz.rng = new ParkMillerRng(quiz.seed); toArray(props.children).forEach(child => { if (Question.is(child)) { quiz.questions.push(QuestionData.parse(quiz, child)); } else if (Description.is(child)) { quiz.descriptions.push(child.props.children); } }); if (quiz.randomOrder) { quiz.sortQuestions(); } return quiz; } title: React.ReactChild; seed: number = new Date().getTime(); rng: Rng = DEFAULT_RNG; randomOrder: boolean = false; autoStart: boolean = false; questions: QuestionData[] = []; descriptions: Array = []; constructor(title: React.ReactChild) { this.title = title; } sortQuestions() { this.questions.sort((a, b) => (this.rng.nextFloat() < 0.5 ? -1 : 1)); } }