Source

externalCode/quiz.js

/**
 * @typedef {"webview" | "htmlparser"} QuestionClosedRenderMode
 */

/**
 * @typedef {Function} OverrideQuestionHTMLWrapperCallback
 * @param {String} HTML Default HTML wrapper
 * @param {String} inputHTML HTML that contains the input element
 * @param {String} css CSS passed to webview by default
 */

/**
 * @typedef {Object} QuizPrevNextComponentProps
 * @property {Function} onQuizClick Function to execute if previous or next button is a quiz
 * @property {Function} onLessonClick Function to execute if previous or next button is a lesson
 * @property {Function} onTopicClick Function to execute if previous or next button is a topic
 * @property {Object} global App global style
 * @property {Object} colors App colors
 * @property {TranslationFunction} t
 * @property {?Object} prevObject Information about previous lesson/topic/quiz
 * @property {?Object} nextObject Information about next lesson/topic/quiz
 * @property {Number} courseId Course id of lesson/topic/quiz
 * @property {Function} nextLockedAlert Shows an alert with information that next object is locked
 */

/**
 * @typedef {Object} QuizHeaderProps
 * @property {QuizViewModel} quiz
 * @property {Number} currentSwiperPosition
 * @property {Object} global App global style
 * @property {Object} labels Learndash labels
 * @property {Object<Array>} questions Current questions in the quiz
 * @property {Number} quizOrder
 * @property {Number} quizTotalCount
 * @property {Object} colors App colors
 * @property {Function} setHeaderHeight Helper function which can be called to set header height
 * @property {Boolean | React.ComponentType} renderQuizTimer Returns `false` if quiz doesn't have a timer set. Will return a component if timer for the quiz is set.
 * @property {TranslationFunction} t
 * @property {NavigationService} navigation
 * @property {Function} onQuizClick Function to execute if previous or next button is a quiz
 * @property {Function} onLessonClick Function to execute if previous or next button is a lesson
 * @property {Function} onTopicClick Function to execute if previous or next button is a topic
 * @property {Number} courseId Course id of lesson/topic/quiz
 * @property {?Object} nextObject Information about next lesson/topic/quiz
 * @property {?Object} prevObject Information about previous lesson/topic/quiz
 * @property {CourseViewModel} course
 * @property {Function} nextLockedAlert Shows an alert with information that next object is locked
 * @property {Boolean} isResultsVisible Returns `true` if screen is currently showing the results of the quiz
 * @property {React.ComponentType} backToCourse Returns default back button component
 * @property {Boolean} hidePrevNext Returns `true` if prev/next buttons should be hidden
 * @property {Boolean | React.ComponentType} prevNext Returns `false` if hidePrevNext is `true`. Will return buttons which can navigate through previous and next screens if hidePrevNext is `false`
 */

/**
 * @typedef {Object} QuizTitleComponentProps
 * @property {QuizHeaderProps} QuizHeaderProps
 */

/**
 * @typedef {Object} QuizScreenHeaderProps
 * @property {QuizHeaderProps} QuizHeaderProps
 * @property {Object} headerLeftStyle Default styling for left section of the header
 * @property {Object} style Default styling of lesson header
 */

/**
 * @class
 * Quiz Hooks.
 * Instance name: quizApi
  
   You can use these hooks to customize the quiz questions for your app.
 * @example
 * externalCodeSetup.quizApi.METHOD_NAME
 */
export class QuizApi {
	questionClosedRenderMode = "webview";

	/**
	 * It overrides the Closed Question type render mode. The default mode is `webview`. If the question is not rendering correctly, you can try setting it to `htmlparser` mode.
	 * @method
	 * @param {QuestionClosedRenderMode} questionClosedRenderMode
	 * @example
	 * externalCodeSetup.quizApi.setQuestionClosedRenderMode("htmlparser")
	 */
	setQuestionClosedRenderMode = questionClosedRenderMode => {
		this.questionClosedRenderMode = questionClosedRenderMode;
	};

	wrapQuestionHtmlFilter = (HTML, inputHTML, css) => HTML;

	/**
	 * We use an HTML wrapper for "Fill in the blank" questions to accept input answers.
	 * You can use this to change the HTML output.
	 * @method
	 * @param {OverrideQuestionHTMLWrapperCallback} wrapQuestionHtmlFilter
	 * @example <caption>Add additional details below the input field</caption>
	 * externalCodeSetup.quizApi.setWrapQuestionHtmlFilter((HTMLWrapper, inputHtml, css) => {
	 *
	 *    const disableZoom = true;
	 *    const myNewHtml =
	 *      `
	 *   		<!DOCTYPE html>
	 *   		<html>
	 *    			<head>
	 *   				<title>Topic Content</title>
	 *   				<meta http-equiv="content-type" content="text/html; charset=utf-8">
	 *   				<meta name="viewport" content="width=device-width, initial-scale=1 ${disableZoom ? "maximum-scale=1.0" : ""} ">
	 *   				<style type="text/css">
	 *   				${css}
	 *   				.content {
	 *   					width: 100%;
	 *   					overflow: hidden;
	 *   					padding-bottom: 4px;
	 *   				}
	 *   				</style>
	 *   			</head>
	 *   			<body>
	 *   				<div class="content">
	 *   				` +
	 *        inputHtml +
	 *      `
	 *        <p> Please use UPPERCASE characters only</p>
	 *   				</div>
	 *   			</body>
	 *   		</html>
	 *   	`;
	 *    return myNewHtml;
	 *  })
	 */
	setWrapQuestionHtmlFilter = wrapQuestionHtmlFilter => {
		this.wrapQuestionHtmlFilter = wrapQuestionHtmlFilter;
	};

	PrevNextComponent = null;

	/**
	 * You can use this to replace the previous and next buttons on the quiz screen.
	 * @method
	 * @param {?React.ComponentType<QuizPrevNextComponentProps>} PrevNextComponent
	 * @example <caption> Change colors of the default previous and next buttons </caption>
	 *
	 * //In custom_code/components/PrevNext.js
	 *
	 * import React from "react";
	 * import { Text, View, StyleSheet } from "react-native";
	 * import AppTouchableOpacity from "@src/components/AppTouchableOpacity";
	 * import Icon from "@src/components/Icon";
	 * import { shadeColor } from "@src/utils";
	 *
	 * export const onObjectClick = (
	 *    object,
	 *    onQuizClick,
	 *    onTopicClick,
	 *    onLessonClick
	 * ) => {
	 *    if (!!!object) {
	 *        return false;
	 *    }
	 *    switch (object.type) {
	 *        case "quiz":
	 *            onQuizClick(object.parentType, object.parent)(object);
	 *            break;
	 *        case "topic":
	 *            onTopicClick(object, object.parent);
	 *            break;
	 *        case "lesson":
	 *            onLessonClick(object);
	 *            break;
	 *    }
	 * };
	 *
	 * const PrevNext = ({
	 *    global,
	 *    colors,
	 *    t,
	 *    prevObject,
	 *    nextObject,
	 *    courseId,
	 *    onQuizClick,
	 *    onLessonClick,
	 *    onTopicClick,
	 *    nextLockedAlert
	 * }) => {
	 *
	 *    return (
	 *        <View style={[global.row]}>
	 *            <AppTouchableOpacity
	 *                style={[
	 *                    global.wrappedButton,
	 *                    global.wrappedTextButton,
	 *                    { marginRight: 4, backgroundColor: "purple" }
	 *                ]}
	 *                onPress={() => {
	 *                    if (prevObject !== "disabled") {
	 *                        onObjectClick(prevObject, onQuizClick, onTopicClick, onLessonClick);
	 *                    }
	 *                }}
	 *            >
	 *                <View style={global.row}>
	 *                    <View style={global.linkWithArrow}>
	 *                        <Text
	 *                            style={[
	 *                                global.wrappedTextButtonLabel,
	 *                                {
	 *                                    color:
	 *                                        !!!prevObject || prevObject === "disabled"
	 *                                            ? shadeColor(colors.headerIconColor, 0.4)
	 *                                            : "red"
	 *                                }
	 *                            ]}
	 *                        >
	 *                            {t("lesson:prevButtonText")}
	 *                        </Text>
	 *                    </View>
	 *                </View>
	 *            </AppTouchableOpacity>
	 *
	 *            <AppTouchableOpacity
	 *                style={[global.wrappedButton, global.wrappedTextButton, {backgroundColor: "purple"}]}
	 *                onPress={() => {
	 *                    if (nextObject !== "disabled") {
	 *                        onObjectClick(nextObject, onQuizClick, onTopicClick, onLessonClick);
	 *                    } else if (typeof nextLockedAlert === "function") {
	 *                        nextLockedAlert();
	 *                    }
	 *                }}
	 *            >
	 *                <View style={global.row}>
	 *                    <View style={global.linkWithArrow}>
	 *                        <Text
	 *                            style={[
	 *                                global.wrappedTextButtonLabel,
	 *                                {
	 *                                    color:
	 *                                        !!!nextObject || nextObject === "disabled"
	 *                                            ? shadeColor(colors.headerIconColor, 0.4)
	 *                                            : "blue"
	 *                                }
	 *                            ]}
	 *                        >
	 *                            {t("lesson:nextButtonText")}
	 *                        </Text>
	 *                    </View>
	 *                </View>
	 *            </AppTouchableOpacity>
	 *        </View>
	 *    );
	 * };
	 *
	 * export default PrevNext;
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 *
	 * import PrevNextComponent from './components/PrevNext';
	 * export const applyCustomCode = externalCodeSetup => {
	 *   externalCodeSetup.quizApi.setPrevNextComponent((props) => <PrevNextComponent {...props} />);
	 * }
	 *
	 */
	setPrevNextComponent = PrevNextComponent => {
		this.PrevNextComponent = PrevNextComponent;
	};

	QuizTitleComponent = null;

	/**
	 * You can use this to replace the quiz's title component.
	 * For example, you can use this to add more details to the quiz's title.
	 * @method
	 * @param {React.ComponentType<QuizTitleComponentProps>} QuizTitleComponent
	 * @example <caption> Add more quiz details to component </caption>
	 *
	 * //In custom_code/components/QuizTitle.js...
	 *
	 * import React from "react";
	 * import {View, Text} from "react-native";
	 * import Animated from "react-native-reanimated";
	 * const QuizTitle = ({
	 *	quiz,
	 *	global,
	 *	colors,
	 *	paddingTop = 14,
	 *	setHeaderHeight,
	 *	t,
	 *	labels,
	 *	questions,
	 *	currentSwiperPosition,
	 *	quizOrder,
	 *	quizTotalCount
	 * }) => {
	 *
	 * const paddingBottom = 14;
	 *
	 * return (
	 *   <Animated.View
	 *     style={[
	 *	   {
	 *       backgroundColor: colors.bodyFrontBg,
	 *       width: "100%",
	 *       shadowOffset: {width: 0, height: 1},
	 *       shadowRadius: 1,
	 *       shadowColor: "#000",
	 *       shadowOpacity: 0.05
	 *     }
	 *     ]}
	 *   >
	 *     <View
	 *       style={[
	 *        global.row,
	 *        {
	 *          justifyContent: "space-between",
	 *          alignItems: "flex-start",
	 *          paddingTop,
	 *          paddingBottom
	 *        }
	 *       ]}
	 *       onLayout={event => {
	 *         const {height} = event.nativeEvent.layout;
	 *         typeof setHeaderHeight === "function" && setHeaderHeight(height);
	 *       }}
	 *     >
	 *     <Animated.View
	 *       style={{
	 *         flex: 1,
	 *         paddingHorizontal: 20
	 *       }}
	 *     >
	 *       <Animated.Text
	 *         style={[
	 *           global.courseHeaderTitle,
	 *           {marginBottom: 5}
	 *         ]}
	 *       >
	 *         {quiz.title}
	 *       </Animated.Text>
	 *
	 *       {
	 *         !quiz.hideQuestionPositionOverview &&
	 *         !quiz.hideQuestionNumbering &&
	 *         currentSwiperPosition > 0 ? (
	 *         <Text style={global.courseHeaderSubTitle}>
	 *           {t("quiz:questionCount", {
	 *             question: labels.question,
	 *             current: currentSwiperPosition,
	 *             total: questions
	 *             ? questions.size
	 *             : ""
	 *           })}
	 *         </Text>
	 *         ) : (
	 *         <Text style={global.courseHeaderSubTitle}>
	 *           {`Quiz ${quizOrder + 1} of ${quizTotalCount}`}
	 *         </Text>
	 *         )
	 *       }
	 *
	 *       <Text style={global.courseHeaderSubTitle}>
	 *         Author: {quiz.author.name}
	 *       </Text>
	 *       <Text style={global.courseHeaderSubTitle}>
	 *         Completed: {quiz.completed.toString()}
	 *       </Text>
	 *
	 *     </Animated.View>
	 *   </View>
	 *  </Animated.View>
	 *  );
	 * };
	 *
	 * export default QuizTitle;
	 *
	 * //In custom_code/index.js...
	 *
	 * ...
	 * import QuizTitle from "./components/QuizTitle";
	 * export const applyCustomCode = externalCodeSetup => {
	 *  externalCodeSetup.quizApi.setQuizTitleComponent(props => <QuizTitle {...props} />)
	 * }
	 *
	 */
	setQuizTitleComponent = QuizTitleComponent => {
		this.QuizTitleComponent = QuizTitleComponent;
	};

	QuizScreenHeader = null;
	/**
	 * You can use this hook to customize the header of the Quiz Single Screen which by default, contains the back-to-course button and the previous/next buttons.
	 * @method
	 * @param {React.ComponentType<QuizScreenHeaderProps>} QuizScreenHeader
	 * @example <caption> Add a timer on the quiz screen header </caption>
	 *
	 * //In custom_code/components/QuizScreenHeader.js...
	 *
	 * import React from "react";
	 * import {View, Text} from "react-native";
	 * import Animated from "react-native-reanimated";
	 * import {DEVICE_WIDTH} from "@src/styles/global";
	 * import AuthWrapper from "@src/components/AuthWrapper";
	 *
	 * const Header = ({
	 *     headerLeftStyle,
	 *     style,
	 *     global,
	 *     colors,
	 *     backToCourse,
	 *     headerRightAuthWrapperProps,
	 *     prevNext,
	 *     quiz,
	 *     renderQuizTimer
	 * }) => {
	 *   return (
	 *     <Animated.View
	 *       style={[
	 *         global.row,
	 *         global.fakeHeader,
	 *         {
	 *           backgroundColor: "transparent",
	 *           paddingHorizontal: 10,
	 *           overflow: "hidden"
	 *         },
	 *         {
	 *           width: DEVICE_WIDTH
	 *         },
	 *         style
	 *       ]}
	 *     >
	 *       <View
	 *         style={[
	 *           {
	 *             alignItems: "center",
	 *             justifyContent: "center",
	 *             flexDirection: "row",
	 *             flex: 1,
	 *             height: "100%",
	 *
	 *                         backgroundColor: "gray",
	 *                         borderRadius: 20
	 *           }
	 *         ]}
	 *       >
	 *         <View style={[global.headerButtonLeft, headerLeftStyle]}>
	 *           {backToCourse}
	 *         </View>
	 *
	 *         <View style={[global.headerCustomTitle]}>
	 *           {renderQuizTimer(quiz, global, colors)}
	 *         </View>
	 *
	 *         <View style={[global.headerButtonRight]}>
	 *           <AuthWrapper
	 *             actionOnGuestLogin={"hide"}
	 *             {...headerRightAuthWrapperProps}
	 *           >
	 *             {prevNext}
	 *           </AuthWrapper>
	 *         </View>
	 *       </View>
	 *     </Animated.View>
	 *   );
	 * };
	 *
	 * export default Header;
	 *
	 *  //In custom_code/index.js...
	 *
	 * import QuizScreenHeader from "./components/QuizScreenHeader";
	 * export const applyCustomCode = externalCodeSetup => {
	 *   externalCodeSetup.quizApi.setQuizScreenHeader(props => <QuizScreenHeader {...props} />)
	 * }
	 *
	 */
	setQuizScreenHeader = QuizScreenHeader => {
		this.QuizScreenHeader = QuizScreenHeader;
	};
}