/** @jsx jsx */ import { jsx, css } from "@emotion/react"; import React, { ChangeEvent } from "react"; import { Question, QuestionType } from "../api/question"; import { selectable } from "../commonStyles"; import create_input from "./InputTypes"; import ErrorMessage from "./ErrorMessage"; const skip_normal_state: Array = [ QuestionType.Radio, QuestionType.Checkbox, QuestionType.Select, QuestionType.TimeZone, QuestionType.Section, QuestionType.Range ]; export interface QuestionState { // Common keys value: string | null | Map // Validation valid: boolean error: string // Unittest-specific validation unittestsFailed: boolean // This indicates a failure in testing when submitting (i.e not from common validation) testFailure: boolean // Whether we had failed unittests, or other failures, such as code loading } export type QuestionProp = { question: Question, scroll_ref: React.RefObject, // eslint-disable-next-line @typescript-eslint/no-explicit-any focus_ref: React.RefObject, selfRef: React.RefObject, } class RenderedQuestion extends React.Component { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: TS2610 state: QuestionState; /** The current state of the question components, which may or may not match the state rendered. */ public realState: QuestionState; setState(state: Partial): void { this.realState = {...this.realState, ...state}; super.setState(state); } constructor(props: QuestionProp) { super(props); if (props.question.type === QuestionType.TextArea) { this.handler = this.text_area_handler.bind(this); } else if (props.question.type === QuestionType.Code) { this.handler = this.code_field_handler.bind(this); } else { this.handler = this.normal_handler.bind(this); } this.blurHandler = this.blurHandler.bind(this); this.state = { value: skip_normal_state.includes(props.question.type) ? null : "", valid: true, error: "", unittestsFailed: false, testFailure: false, }; this.realState = this.state; } // This is here to allow dynamic selection between the general handler, textarea, and code field handlers. handler(_: ChangeEvent | string): void {} // eslint-disable-line blurHandler(): void { if (this.props.question.required) { if (!this.realState.value) { this.setState({ error: "Field must be filled.", valid: false }); } else { this.setState({ error: "", valid: true, unittestsFailed: false }); } } } normal_handler(event: ChangeEvent): void { switch (event.target.type) { case "checkbox": if (!(this.realState.value instanceof Map)) return; this.realState.value.set(event.target.name, event.target.checked); break; case "radio": { // This handles radios and ranges, as they are both based on the same fundamental input type let value; if (event.target.parentElement) { value = event.target.parentElement.innerText.trimEnd(); } else { value = event.target.value; } this.setState({value: value}); break; } default: this.setState({value: event.target.value}); } // Toggle checkbox class if (event.target.type === "checkbox" && event.target.parentElement !== null) { event.target.parentElement.classList.toggle("unselected"); event.target.parentElement.classList.toggle("selected"); } const options: string | string[] = this.props.question.data["options"]; switch (event.target.type) { case "text": this.setState({valid: true}); break; case "checkbox": // We need to check this here, because checkbox doesn't have onBlur if (this.props.question.required && typeof options !== "string") { if (!(this.realState.value instanceof Map)) return; const valid = Array.from(this.realState.value.values()).includes(true); this.setState({ error: valid ? "" : "Field must be filled.", valid: valid }); } break; case "radio": this.setState({ valid: true, error: "" }); break; } } text_area_handler(event: ChangeEvent): void { // We will validate again when focusing out. this.setState({ value: event.target.value, valid: true, error: "" }); } code_field_handler(newContent: string): void { // If content stays same (what means that user have just zoomed in), then don't validate. let validate = false; if (newContent != this.realState.value) { validate = true; } this.setState({value: newContent}); // CodeMirror don't provide onBlur event, so we have to run validation here. if (validate) { this.blurHandler(); } } validateField(): void { if (!this.props.question.required) { return; } let valid = true; const options: string | string[] = this.props.question.data["options"]; switch (this.props.question.type) { case QuestionType.TextArea: case QuestionType.ShortText: case QuestionType.Code: if (this.realState.value === "") { valid = false; } break; case QuestionType.Select: case QuestionType.Range: case QuestionType.TimeZone: case QuestionType.Radio: if (!this.realState.value) { valid = false; } break; case QuestionType.Checkbox: if (typeof options !== "string") { if (!(this.realState.value instanceof Map)) return; valid = Array.from(this.realState.value.values()).includes(true); } break; } this.setState({ error: valid ? "" : "Field must be filled", valid: valid }); } componentDidMount(): void { // Initialize defaults for complex and nested fields const options: string | string[] = this.props.question.data["options"]; switch (this.props.question.type) { case QuestionType.Checkbox: if (typeof options === "string") return; this.setState({ value: new Map(options.map((option, index) => [`${("000" + index).slice(-4)}. ${option}`, false] )) }); break; } } generateUnitTestErrorMessage(): JSX.Element { let inner; if (this.realState.testFailure) { inner =
{"Unittest Failure:\n"}
    {this.realState.error.split(";").map(testName =>
  • {testName}
  • )}
; } else { inner = `Unittest Failure:\n\n${this.realState.error}`; } const element =
{inner}
; return ; } render(): JSX.Element { const question = this.props.question; const name = question.name.split("\n").map((line, index) => {line}
); name.push({name.pop()?.props.children[0]}); if (question.type === QuestionType.Section) { const styles = css` h1 { margin-bottom: 0; } h3 { margin-top: 0; } h1, h3 { text-align: center; padding: 0 2rem; } @media (max-width: 500px) { h1, h3 { padding: 0; } } `; const data = question.data["text"]; let text; if (data && typeof(data) === "string") { text = data.split("\n").map((line, index) =>

{line}

); text.push(

{text.pop()?.props.children[0]}

); } else { text = ""; } return

{name}

{ text }
; } else { const requiredStarStyles = css` .required { display: inline-block; position: relative; color: red; top: -0.2rem; margin-left: 0.2rem; } `; let error; if (this.props.question.type === QuestionType.Code && this.realState.unittestsFailed) { error = this.generateUnitTestErrorMessage(); } else { error = ; } return

{name}*

{ create_input(this, this.handler, this.blurHandler, this.props.focus_ref) } {error}
; } } } export default RenderedQuestion;