190 lines
4.8 KiB
TypeScript
190 lines
4.8 KiB
TypeScript
import { UI_DIMENSIONS, UI_TEXT } from "../../constants/ui.ts";
|
||
import { isOptionSelected } from "../../state/answers.ts";
|
||
import {
|
||
getAnswer,
|
||
getOptionNote,
|
||
getQuestionNote,
|
||
isInputOpenForQuestion,
|
||
isOptionNoteOpen,
|
||
isQuestionNoteOpen,
|
||
} from "../../state/selectors.ts";
|
||
import type { AskDisplayOption } from "../../types.ts";
|
||
import type { QuestionRenderContext } from "../render-types.ts";
|
||
|
||
export type QuestionNoteModel =
|
||
| { kind: "editor"; placeholder: string }
|
||
| { kind: "saved"; text: string };
|
||
|
||
export type OptionDetailModel =
|
||
| { kind: "editor"; placeholder: string; withGap: boolean }
|
||
| { kind: "saved-note"; text: string; withGap: boolean }
|
||
| { kind: "custom-text"; text: string; withGap: boolean };
|
||
|
||
export interface OptionRowModel {
|
||
color: "accent" | "text" | "success";
|
||
description?: string;
|
||
detail?: OptionDetailModel;
|
||
index: number;
|
||
isCustom: boolean;
|
||
label: string;
|
||
pointer: string;
|
||
prefix: string;
|
||
selected: boolean;
|
||
}
|
||
|
||
export interface QuestionScreenModel {
|
||
mode: "preview" | "standard";
|
||
previewLayout?: "custom" | "stacked" | "wide";
|
||
questionNote?: QuestionNoteModel;
|
||
rows: OptionRowModel[];
|
||
selectedOption?: AskDisplayOption;
|
||
selectedOptionDetail?: OptionDetailModel;
|
||
}
|
||
|
||
export function buildQuestionScreenModel(
|
||
context: QuestionRenderContext
|
||
): QuestionScreenModel {
|
||
const rows = context.options.map((option, index) =>
|
||
buildOptionRowModel(context, option, index)
|
||
);
|
||
const questionNote = buildQuestionNoteModel(context);
|
||
if (context.question.type !== "preview") {
|
||
return {
|
||
mode: "standard",
|
||
questionNote,
|
||
rows,
|
||
};
|
||
}
|
||
|
||
const selectedOption = context.options[context.state.activeOptionIndex];
|
||
return {
|
||
mode: "preview",
|
||
previewLayout: getPreviewLayout(context, selectedOption),
|
||
questionNote,
|
||
rows,
|
||
selectedOption,
|
||
selectedOptionDetail: buildOptionDetailModel(context, selectedOption, true),
|
||
};
|
||
}
|
||
|
||
function buildQuestionNoteModel(
|
||
context: QuestionRenderContext
|
||
): QuestionNoteModel | undefined {
|
||
const { state, question } = context;
|
||
if (isQuestionNoteOpen(state, question.id)) {
|
||
return { kind: "editor", placeholder: UI_TEXT.editorPlaceholderNote };
|
||
}
|
||
const note = getQuestionNote(state, question.id);
|
||
return note ? { kind: "saved", text: note } : undefined;
|
||
}
|
||
|
||
function buildOptionRowModel(
|
||
context: QuestionRenderContext,
|
||
option: AskDisplayOption,
|
||
index: number
|
||
): OptionRowModel {
|
||
const { question, state } = context;
|
||
const answer = getAnswer(state, question.id);
|
||
const selected = index === state.activeOptionIndex;
|
||
const answered = option.isCustomOption
|
||
? !!answer?.customText
|
||
: isOptionSelected(answer, option.value);
|
||
return {
|
||
color: getOptionColor(answered, selected),
|
||
description: option.description,
|
||
detail:
|
||
context.question.type === "preview"
|
||
? undefined
|
||
: buildOptionDetailModel(context, option, selected),
|
||
index,
|
||
isCustom: !!option.isCustomOption,
|
||
label: option.label,
|
||
pointer: selected ? "❯ " : " ",
|
||
prefix: getOptionPrefix(question.type, option, answered),
|
||
selected,
|
||
};
|
||
}
|
||
|
||
function buildOptionDetailModel(
|
||
context: QuestionRenderContext,
|
||
option: AskDisplayOption | undefined,
|
||
selected: boolean
|
||
): OptionDetailModel | undefined {
|
||
if (!option) {
|
||
return;
|
||
}
|
||
|
||
const { state, question } = context;
|
||
const answer = getAnswer(state, question.id);
|
||
const inputOpen =
|
||
!!option.isCustomOption &&
|
||
selected &&
|
||
isInputOpenForQuestion(state, question.id);
|
||
const noteOpen =
|
||
!option.isCustomOption &&
|
||
isOptionNoteOpen(state, question.id, option.value);
|
||
const customText = option.isCustomOption ? answer?.customText : undefined;
|
||
const note = option.isCustomOption
|
||
? undefined
|
||
: getOptionNote(state, question.id, option.value);
|
||
|
||
if (noteOpen) {
|
||
return {
|
||
kind: "editor",
|
||
placeholder: UI_TEXT.editorPlaceholderNote,
|
||
withGap: false,
|
||
};
|
||
}
|
||
if (inputOpen) {
|
||
return {
|
||
kind: "editor",
|
||
placeholder: UI_TEXT.editorPlaceholderInput,
|
||
withGap: false,
|
||
};
|
||
}
|
||
if (customText) {
|
||
return { kind: "custom-text", text: customText, withGap: false };
|
||
}
|
||
if (note) {
|
||
return { kind: "saved-note", text: note, withGap: false };
|
||
}
|
||
return;
|
||
}
|
||
|
||
function getPreviewLayout(
|
||
context: QuestionRenderContext,
|
||
selectedOption: AskDisplayOption | undefined
|
||
): "custom" | "stacked" | "wide" {
|
||
if (selectedOption?.isCustomOption) {
|
||
return "custom";
|
||
}
|
||
return context.width >= UI_DIMENSIONS.previewWideMinWidth &&
|
||
context.options.length > 0
|
||
? "wide"
|
||
: "stacked";
|
||
}
|
||
|
||
function getOptionPrefix(
|
||
questionType: QuestionRenderContext["question"]["type"],
|
||
option: AskDisplayOption,
|
||
answered: boolean
|
||
): string {
|
||
if (questionType !== "multi" || option.isCustomOption) {
|
||
return "";
|
||
}
|
||
return `[${answered ? "x" : " "}] `;
|
||
}
|
||
|
||
function getOptionColor(
|
||
answered: boolean,
|
||
selected: boolean
|
||
): "accent" | "text" | "success" {
|
||
if (answered) {
|
||
return "success";
|
||
}
|
||
if (selected) {
|
||
return "accent";
|
||
}
|
||
return "text";
|
||
}
|