diff --git a/packages/tdev/page-index/remark-plugin/index.ts b/packages/tdev/page-index/remark-plugin/index.ts index ff737316e..744a09701 100644 --- a/packages/tdev/page-index/remark-plugin/index.ts +++ b/packages/tdev/page-index/remark-plugin/index.ts @@ -61,10 +61,6 @@ const scheduleExportDb = debounce( { edges: ['trailing'] } ); -/** - * This plugin transforms inline code and code blocks in MDX files to use - * custom MDX components by converting the code content into attributes. - */ const remarkPlugin: Plugin = function plugin( options = { components: [], persistedCodeType: () => 'code' } ): Transformer { diff --git a/packages/tdev/page-read-check/PageReadCheck/index.tsx b/packages/tdev/page-read-check/PageReadCheck/index.tsx index 0316a20a8..925faa514 100644 --- a/packages/tdev/page-read-check/PageReadCheck/index.tsx +++ b/packages/tdev/page-read-check/PageReadCheck/index.tsx @@ -10,6 +10,7 @@ import clsx from 'clsx'; import { mdiFlashTriangle } from '@mdi/js'; import Icon from '@mdi/react'; import PageReadChecker from '../model'; +import { useScrollTo } from '@tdev-hooks/useScrollTo'; interface Props extends MetaInit { id: string; @@ -28,11 +29,10 @@ const defaultDisabledReason = (doc: PageReadChecker) => const PageReadCheck = observer((props: Props) => { const { text = defaultText, disabledReason = defaultDisabledReason } = props; const [meta] = React.useState(new ModelMeta(props)); - const ref = React.useRef(null); const viewStore = useStore('viewStore'); const doc = useFirstMainDocument(props.id, meta); - const [animate, setAnimate] = React.useState(false); + const [ref, animate] = useScrollTo(doc); React.useEffect(() => { if (!viewStore.isPageVisible || !doc) { @@ -49,25 +49,6 @@ const PageReadCheck = observer((props: Props) => { }; }, [doc, doc?.read, viewStore.isPageVisible, props.continueAfterUnlock]); - React.useEffect(() => { - if (ref.current && doc?.scrollTo) { - ref.current.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' }); - doc.setScrollTo(false); - setAnimate(true); - } - }, [ref, doc?.scrollTo]); - - React.useEffect(() => { - if (animate) { - const timeout = setTimeout(() => { - setAnimate(false); - }, 2000); - return () => { - clearTimeout(timeout); - }; - } - }, [animate]); - if (!doc) { return null; } diff --git a/packages/tdev/page-read-check/model/index.ts b/packages/tdev/page-read-check/model/index.ts index 28cfe61fa..cae1fb48e 100644 --- a/packages/tdev/page-read-check/model/index.ts +++ b/packages/tdev/page-read-check/model/index.ts @@ -12,6 +12,7 @@ export const createModel: Factory = (data, store) => { }; class PageReadChecker extends iDocument<'page_read_check'> implements iTaskableDocument<'page_read_check'> { + readonly hideFromOverview = false; @observable accessor readTime: number = 0; @observable accessor read: boolean = false; @observable accessor scrollTo: boolean = false; diff --git a/src/api/document.ts b/src/api/document.ts index baf6b84cb..eb55dc6fc 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -19,11 +19,10 @@ import iDocumentContainer from '@tdev-models/iDocumentContainer'; import iViewStore from '@tdev-stores/ViewStores/iViewStore'; import Code from '@tdev-models/documents/Code'; import { iTaskableDocument } from '@tdev-models/iTaskableDocument'; -import ChoiceAnswer, { - ChoiceAnswerChoices, - ChoiceAnswerOptionOrders, - ChoiceAnswerQuestionOrder -} from '@tdev-models/documents/ChoiceAnswer'; +import type ChoiceAnswer from '@tdev-models/documents/Assessable/ChoiceAnswer'; +import type TrueFalseAnswer from '@tdev-models/documents/Assessable/TrueFalseAnswer'; +import type Quiz from '@tdev-models/documents/Assessable/Quiz'; +import iAssessable from '@tdev-models/documents/Assessable/iAssessable'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -46,11 +45,22 @@ export interface StringData { text: string; } -export interface ChoiceAnswerData { - choices: ChoiceAnswerChoices; - optionOrders: ChoiceAnswerOptionOrders; - questionOrder: ChoiceAnswerQuestionOrder | null; +interface AssessableData { assessed: boolean; + qid?: string; +} + +export interface TrueFalseAnswerData extends AssessableData { + value: boolean | null; +} + +export interface ChoiceAnswerData extends AssessableData { + choices: number[]; + optionsOrder: number[]; +} + +export interface QuizData extends AssessableData { + questionOrder: number[]; } export interface QuillV2Data { @@ -122,11 +132,17 @@ export interface ViewStoreTypeMapping { export type ViewStoreType = keyof ViewStoreTypeMapping; export type ViewStore = ViewStoreTypeMapping[ViewStoreType]; +export interface AssessableDataMapping { + ['true_false_answer']: TrueFalseAnswerData; + ['choice_answer']: ChoiceAnswerData; + ['quiz']: QuizData; +} + export interface ContainerTypeDataMapping { ['_container_placeholder_']: { name: string }; // placeholder to avoid empty interface error } -export interface TaskableDocumentMapping { +export interface TaskableDocumentMapping extends AssessableDataMapping { ['task_state']: TaskStateData; ['progress_state']: ProgressStateData; } @@ -136,7 +152,6 @@ export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeD // TODO: rename to `code_version`? ['script_version']: ScriptVersionData; ['string']: StringData; - ['choice_answer']: ChoiceAnswerData; ['quill_v2']: QuillV2Data; ['solution']: SolutionData; ['dir']: DirData; @@ -149,6 +164,7 @@ export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeD } export type ContainerType = keyof ContainerTypeDataMapping; export type TaskableType = keyof TaskableDocumentMapping; +export type AssessableType = keyof AssessableDataMapping; type KeysWithCode = { [K in keyof T]: 'code' extends keyof T[K] ? K : never; @@ -160,7 +176,16 @@ export interface ContainerTypeModelMapping { ['_container_placeholder_']: iDocumentContainer; // placeholder to avoid empty interface error } -export interface TaskableTypeModelMapping { +export interface AssessableTypeModelMapping { + ['true_false_answer']: TrueFalseAnswer; + ['choice_answer']: ChoiceAnswer; + ['quiz']: Quiz; +} +// enforce all AssessableTypeModels to extend iAssessable: +type EnsureAllAssessable }> = T; +null as unknown as EnsureAllAssessable; + +export interface TaskableTypeModelMapping extends AssessableTypeModelMapping { ['task_state']: TaskState; ['progress_state']: ProgressState; } @@ -168,12 +193,12 @@ export interface TaskableTypeModelMapping { type EnsureAllTaskable }> = T; null as unknown as EnsureAllTaskable; -export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTypeModelMapping { +export interface TypeModelMapping + extends TaskableTypeModelMapping, ContainerTypeModelMapping, AssessableTypeModelMapping { ['code']: Code; // TODO: rename to `code_version`? ['script_version']: ScriptVersion; ['string']: String; - ['choice_answer']: ChoiceAnswer; ['quill_v2']: QuillV2; ['solution']: Solution; ['dir']: Directory; @@ -192,6 +217,7 @@ export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTyp export type ContainerModelType = ContainerTypeModelMapping[ContainerType]; export type TaskableModelType = TaskableTypeModelMapping[TaskableType]; +export type AssessableModelType = AssessableTypeModelMapping[AssessableType]; export type DocumentType = keyof TypeModelMapping; export type DocumentModelType = TypeModelMapping[DocumentType]; diff --git a/src/components/documents/Assessable/ChoiceAnswer/index.tsx b/src/components/documents/Assessable/ChoiceAnswer/index.tsx new file mode 100644 index 000000000..029f07a45 --- /dev/null +++ b/src/components/documents/Assessable/ChoiceAnswer/index.tsx @@ -0,0 +1,83 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentType'; +import Loader from '@tdev-components/Loader'; +import useIsBrowser from '@docusaurus/useIsBrowser'; +import { useDocumentRootId } from '@tdev-hooks/useContextDocumentRootId'; +import { useFirstDocumentBy } from '@tdev-hooks/useFirstDocumentBy'; +import { DocContext } from '@tdev-components/documents/DocumentContext'; +import { AssessableComponentProps } from '@tdev-models/documents/Assessable/AssessableMeta'; +import { type default as ChoiceAnswerModel, ModelMeta } from '@tdev-models/documents/Assessable/ChoiceAnswer'; +import Options from '../Inputs/Options'; +import Option, { Props as OptionProps } from '../Inputs/Option'; +import QuestionCard from '../QuestionCard'; +import { action } from 'mobx'; +import { useDocument } from '@tdev-hooks/useContextDocument'; + +interface SharedProps extends AssessableComponentProps<'choice_answer'> { + multiple?: boolean; + randomizeOptions?: boolean; + optionsCount: number; +} + +export interface StandaloneProps extends SharedProps { + id: string; + qid: never; +} + +export interface InQuizProps extends SharedProps { + qid: string; +} + +export type ChoiceAnswerProps = StandaloneProps | InQuizProps; + +interface ThinWrapperProps { + children: React.ReactNode; +} + +type ChoiceAnswerSubComponents = { + Options: React.FC; + Option: React.FC>; +}; + +const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { + const [meta] = React.useState(new ModelMeta(props)); + const docRootId = useDocumentRootId(props.id); + + const doc = useFirstDocumentBy(docRootId, meta, props.qid); + const isBrowser = useIsBrowser(); + + if (!doc) { + return ; + } + + if (!isBrowser) { + return ; + } + + return ( + + {props.children} + + ); +}) as React.FC & ChoiceAnswerSubComponents; + +ChoiceAnswer.Options = Options; + +const onUpdateSelection = action((doc: ChoiceAnswerModel, optionIndex: number, checked: boolean) => { + doc.updateSelection(optionIndex, checked, doc.multiple); +}); +ChoiceAnswer.Option = observer((props: Omit, 'onChange'>) => { + const doc = useDocument<'choice_answer'>(); + return ( +