diff --git a/assets/core/scss/themes/_dark.scss b/assets/core/scss/themes/_dark.scss index e4b68d0509..2f5302b4bd 100644 --- a/assets/core/scss/themes/_dark.scss +++ b/assets/core/scss/themes/_dark.scss @@ -64,6 +64,7 @@ --tutor-icon-disabled: #{$tutor-gray-600}; --tutor-icon-brand: #{$tutor-brand-600}; --tutor-icon-brand-hover: #{$tutor-brand-700}; + --tutor-icon-brand-secondary: #{$tutor-brand-300}; --tutor-icon-exception1: #{$tutor-exception-1}; --tutor-icon-exception2: #{$tutor-exception-2}; --tutor-icon-success-primary: #{$tutor-success-600}; diff --git a/assets/core/scss/themes/_light.scss b/assets/core/scss/themes/_light.scss index ed12beb47f..af7fb4cca0 100644 --- a/assets/core/scss/themes/_light.scss +++ b/assets/core/scss/themes/_light.scss @@ -65,6 +65,7 @@ --tutor-icon-disabled: #{$tutor-gray-300}; --tutor-icon-brand: #{$tutor-brand-600}; --tutor-icon-brand-hover: #{$tutor-brand-700}; + --tutor-icon-brand-secondary: #{$tutor-brand-300}; --tutor-icon-exception1: #{$tutor-exception-1}; --tutor-icon-exception2: #{$tutor-exception-2}; --tutor-icon-success-primary: #{$tutor-success-700}; diff --git a/assets/core/scss/tokens/_icons.scss b/assets/core/scss/tokens/_icons.scss index 890b6cd68e..3916fb3bf5 100644 --- a/assets/core/scss/tokens/_icons.scss +++ b/assets/core/scss/tokens/_icons.scss @@ -12,6 +12,7 @@ $tutor-icon-secondary: var(--tutor-icon-secondary); $tutor-icon-subdued: var(--tutor-icon-subdued); $tutor-icon-brand: var(--tutor-icon-brand); $tutor-icon-brand-hover: var(--tutor-icon-brand-hover); +$tutor-icon-brand-secondary: var(--tutor-icon-brand-secondary); $tutor-icon-success-primary: var(--tutor-icon-success-primary); $tutor-icon-critical: var(--tutor-icon-critical); $tutor-icon-critical-hover: var(--tutor-icon-critical-hover); @@ -35,6 +36,7 @@ $tutor-icons: ( subdued: $tutor-icon-subdued, brand: $tutor-icon-brand, brand-hover: $tutor-icon-brand-hover, + brand-secondary: $tutor-icon-brand-secondary, success-primary: $tutor-icon-success-primary, critical: $tutor-icon-critical, critical-hover: $tutor-icon-critical-hover, @@ -44,4 +46,4 @@ $tutor-icons: ( exception2: $tutor-icon-exception2, exception4: $tutor-icon-exception4, disabled: $tutor-icon-disabled, -); \ No newline at end of file +); diff --git a/assets/icons/clock-frame.svg b/assets/icons/clock-frame.svg new file mode 100644 index 0000000000..0e1cd6bdc5 --- /dev/null +++ b/assets/icons/clock-frame.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/src/js/frontend/learning-area/quiz/constants.ts b/assets/src/js/frontend/learning-area/quiz/constants.ts index c2d18708ac..77e27bd68c 100644 --- a/assets/src/js/frontend/learning-area/quiz/constants.ts +++ b/assets/src/js/frontend/learning-area/quiz/constants.ts @@ -1,4 +1,3 @@ -export type QuizFooterPosition = 'only' | 'first' | 'middle' | 'last'; export type RevealQuestionType = (typeof QUIZ_REVEAL_CONFIG.SUPPORTED_TYPES)[number]; export const QUIZ_REVEAL_CONFIG = { @@ -13,6 +12,7 @@ export const QUIZ_REVEAL_CONFIG = { EXPLANATION_CONTENT_DATASET: 'quizExplanationContent', DATA_OPTION_ATTR: 'data-option', DATA_REVEALED_ATTR: 'data-revealed', + DATA_RESULT_ATTR: 'data-reveal-result', DATA_OPTION_CORRECT: 'correct', DATA_OPTION_INCORRECT: 'incorrect', } as const; @@ -22,13 +22,6 @@ export const QUIZ_ABANDON_CONFIG = { IGNORE_ANCHOR_PREFIXES: ['#', 'javascript:'], } as const; -export const QUIZ_FOOTER_POSITIONS = { - ONLY: 'only', - FIRST: 'first', - MIDDLE: 'middle', - LAST: 'last', -} as const; - export const QuestionTimeoutAction = { AUTO_ABANDON: 'auto_abandon', AUTO_SUBMIT: 'auto_submit', diff --git a/assets/src/js/frontend/learning-area/quiz/helpers.ts b/assets/src/js/frontend/learning-area/quiz/helpers.ts index 4d446f7dbd..41ec753297 100644 --- a/assets/src/js/frontend/learning-area/quiz/helpers.ts +++ b/assets/src/js/frontend/learning-area/quiz/helpers.ts @@ -51,6 +51,8 @@ export const revealQuestionWithAnswers = (wrapper: HTMLElement, revealAnswerIds: } const inputs = Array.from(question.querySelectorAll('input[type="radio"], input[type="checkbox"]')); + const selectedAnswerIds = new Set(); + const correctAnswerIds = new Set(); inputs.forEach((input) => { const option = input.closest(QUIZ_REVEAL_CONFIG.OPTION_SELECTOR) as HTMLElement | null; @@ -59,7 +61,18 @@ export const revealQuestionWithAnswers = (wrapper: HTMLElement, revealAnswerIds: } const answerId = Number(input.value); + if (Number.isNaN(answerId)) { + return; + } + + if (input.checked) { + selectedAnswerIds.add(answerId); + } + const isCorrect = revealAnswerIds.includes(answerId); + if (isCorrect) { + correctAnswerIds.add(answerId); + } if (isCorrect) { option.setAttribute(QUIZ_REVEAL_CONFIG.DATA_OPTION_ATTR, QUIZ_REVEAL_CONFIG.DATA_OPTION_CORRECT); @@ -70,5 +83,14 @@ export const revealQuestionWithAnswers = (wrapper: HTMLElement, revealAnswerIds: input.disabled = true; }); + const hasMatchingSelection = + selectedAnswerIds.size > 0 && + selectedAnswerIds.size === correctAnswerIds.size && + Array.from(selectedAnswerIds).every((id) => correctAnswerIds.has(id)); + + question.setAttribute( + QUIZ_REVEAL_CONFIG.DATA_RESULT_ATTR, + hasMatchingSelection ? QUIZ_REVEAL_CONFIG.DATA_OPTION_CORRECT : QUIZ_REVEAL_CONFIG.DATA_OPTION_INCORRECT, + ); question.setAttribute(QUIZ_REVEAL_CONFIG.DATA_REVEALED_ATTR, '1'); }; diff --git a/assets/src/js/frontend/learning-area/quiz/layout.ts b/assets/src/js/frontend/learning-area/quiz/layout.ts index e3ae44b413..1b6c6cec1e 100644 --- a/assets/src/js/frontend/learning-area/quiz/layout.ts +++ b/assets/src/js/frontend/learning-area/quiz/layout.ts @@ -1,14 +1,7 @@ import type { AlpineComponentMeta } from '@Core/ts/types'; import { tutorConfig } from '@TutorShared/config/config'; -import { - QUIZ_FOOTER_POSITIONS, - QUIZ_LAYOUT_KEYS, - QUIZ_LAYOUT_SELECTORS, - QUIZ_REVEAL_CONFIG, - QuizLayoutType, - type QuizFooterPosition, -} from './constants'; +import { QUIZ_LAYOUT_KEYS, QUIZ_LAYOUT_SELECTORS, QUIZ_REVEAL_CONFIG, QuizLayoutType } from './constants'; import { revealQuestionWithAnswers } from './helpers'; export interface QuizLayoutConfig { @@ -31,6 +24,10 @@ const quizLayout = (config: QuizLayoutConfig) => { feedbackMode: config.feedbackMode ?? '', revealWaitMs: config.revealWaitMs ?? null, revealAnswerIds: [] as number[], + answerRequiredByIndex: {} as Record, + revealStateByIndex: {} as Record, + skippedByIndex: {} as Record, + revealFooterState: '' as '' | 'correct' | 'incorrect', isRevealing: false, $el: null as HTMLElement | null, @@ -38,10 +35,14 @@ const quizLayout = (config: QuizLayoutConfig) => { init() { this.revealAnswerIds = this.getRevealAnswerIds(); + this.answerRequiredByIndex = this.getAnswerRequiredMap(); + this.revealStateByIndex = this.getRevealStateMap(); + this.skippedByIndex = this.getSkippedStateMap(); if (this.layout === QuizLayoutType.QUESTION_BELOW_EACH_OTHER) { return; } this.currentIndex = 1; + this.syncCurrentRevealFooterState(); }, isQuestionActive(index: number) { @@ -55,19 +56,145 @@ const quizLayout = (config: QuizLayoutConfig) => { if (this.layout === QuizLayoutType.QUESTION_BELOW_EACH_OTHER) { return false; } + if (index >= this.totalQuestions) { + return false; + } + + if (Object.prototype.hasOwnProperty.call(this.answerRequiredByIndex, index)) { + return !this.answerRequiredByIndex[index]; + } + const wrapper = this.getQuestionWrapper(index); - if (!wrapper || index >= this.totalQuestions) { + if (!wrapper) { return false; } return wrapper.dataset.answerRequired !== '1'; }, + hasAttemptedValue(value: unknown): boolean { + if (value === null || value === undefined) { + return false; + } + + if (typeof value === 'string') { + return value.trim().length > 0; + } + + if (Array.isArray(value)) { + return value.some((item) => this.hasAttemptedValue(item)); + } + + if (typeof value === 'object') { + return Object.values(value as Record).some((item) => this.hasAttemptedValue(item)); + } + + return true; + }, + + isQuestionAttempted(index: number): boolean { + if (!form || !this.formId || !form.hasForm(this.formId)) { + return false; + } + + const values = form.getFormState(this.formId).values ?? {}; + const fieldNames = this.getQuestionFieldNames(values, index); + + if (!fieldNames.length) { + return false; + } + + return fieldNames.some((fieldName) => this.hasAttemptedValue(values[fieldName])); + }, + + shouldDisableNextButton(): boolean { + if (this.layout !== QuizLayoutType.SINGLE_QUESTION) { + return false; + } + + return !this.isQuestionAttempted(this.currentIndex); + }, + + getPaginationState(index: number): 'answered' | 'correct' | 'incorrect' | 'skipped' | null { + if (this.revealStateByIndex[index]) { + return this.revealStateByIndex[index]; + } + + if (this.isQuestionAttempted(index)) { + return 'answered'; + } + + if (this.skippedByIndex[index]) { + return 'skipped'; + } + + return null; + }, + + getPaginationItemClass(index: number) { + const state = this.getPaginationState(index); + + return { + active: this.currentIndex === index, + answered: state === 'answered', + correct: state === 'correct', + incorrect: state === 'incorrect', + skipped: state === 'skipped', + upcoming: index > this.currentIndex && state === null, + }; + }, + + syncRevealFooterState(wrapper: HTMLElement) { + if (!this.isRevealMode()) { + this.revealFooterState = ''; + return; + } + + const question = this.getQuestionElement(wrapper); + if (!question || question.getAttribute(QUIZ_REVEAL_CONFIG.DATA_REVEALED_ATTR) !== '1') { + this.revealFooterState = ''; + return; + } + + const result = question.getAttribute(QUIZ_REVEAL_CONFIG.DATA_RESULT_ATTR); + if (result === QUIZ_REVEAL_CONFIG.DATA_OPTION_CORRECT) { + this.revealStateByIndex[this.currentIndex] = 'correct'; + this.revealFooterState = 'correct'; + return; + } + if (result === QUIZ_REVEAL_CONFIG.DATA_OPTION_INCORRECT) { + this.revealStateByIndex[this.currentIndex] = 'incorrect'; + this.revealFooterState = 'incorrect'; + return; + } + + this.revealFooterState = ''; + }, + + syncCurrentRevealFooterState() { + if (!this.isRevealMode()) { + this.revealFooterState = ''; + return; + } + + const wrapper = this.getQuestionWrapper(this.currentIndex); + if (!wrapper) { + this.revealFooterState = ''; + return; + } + + this.syncRevealFooterState(wrapper); + }, + goPrev() { if (this.layout === QuizLayoutType.QUESTION_BELOW_EACH_OTHER) { return; } if (this.currentIndex > 1) { - this.currentIndex -= 1; + this.markCurrentAsSkipped(); + this.runWithViewTransition(() => { + this.currentIndex -= 1; + this.syncCurrentRevealFooterState(); + }, 'back'); this.scrollToQuestion(); } }, @@ -79,6 +206,9 @@ const quizLayout = (config: QuizLayoutConfig) => { if (this.isRevealing) { return; } + if (!skipValidation && this.shouldDisableNextButton()) { + return; + } const wrapper = this.getQuestionWrapper(this.currentIndex); if (!wrapper) { @@ -94,11 +224,16 @@ const quizLayout = (config: QuizLayoutConfig) => { if (!skipValidation && this.isRevealMode() && this.shouldReveal(wrapper)) { this.isRevealing = true; this.revealQuestion(wrapper); + this.syncRevealFooterState(wrapper); const wait = this.getRevealWaitTime(); window.setTimeout(() => { this.isRevealing = false; if (this.currentIndex < this.totalQuestions) { - this.currentIndex += 1; + this.markCurrentAsSkipped(); + this.runWithViewTransition(() => { + this.currentIndex += 1; + this.syncCurrentRevealFooterState(); + }); this.scrollToQuestion(); } }, wait); @@ -106,33 +241,36 @@ const quizLayout = (config: QuizLayoutConfig) => { } if (this.currentIndex < this.totalQuestions) { - this.currentIndex += 1; + this.markCurrentAsSkipped(); + this.runWithViewTransition(() => { + this.currentIndex += 1; + this.syncCurrentRevealFooterState(); + }); this.scrollToQuestion(); } }, goTo(index: number) { - if (this.layout !== QuizLayoutType.QUESTION_PAGINATION) { + if (this.layout === QuizLayoutType.QUESTION_BELOW_EACH_OTHER) { return; } if (!index || index < 1 || index > this.totalQuestions) { return; } - this.currentIndex = index; + this.markCurrentAsSkipped(); + this.runWithViewTransition( + () => { + this.currentIndex = index; + this.syncCurrentRevealFooterState(); + }, + index < this.currentIndex ? 'back' : 'forward', + ); this.scrollToQuestion(); }, - getFooterPosition(): QuizFooterPosition { - if (this.totalQuestions === 1) { - return QUIZ_FOOTER_POSITIONS.ONLY; - } - if (this.currentIndex === 1) { - return QUIZ_FOOTER_POSITIONS.FIRST; - } - if (this.currentIndex >= this.totalQuestions) { - return QUIZ_FOOTER_POSITIONS.LAST; - } - return QUIZ_FOOTER_POSITIONS.MIDDLE; + runWithViewTransition(update: () => void, direction: 'forward' | 'back' = 'forward') { + void direction; + update(); }, getRevealWaitTime(): number { @@ -235,6 +373,94 @@ const quizLayout = (config: QuizLayoutConfig) => { ) as HTMLElement | null; }, + getAnswerRequiredMap(): Record { + const root = this.$root ?? this.$el; + if (!root) { + return {}; + } + + const wrappers = Array.from(root.querySelectorAll(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER)); + const map: Record = {}; + + wrappers.forEach((wrapper) => { + const index = Number(wrapper.getAttribute(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER_ATTR)); + if (Number.isNaN(index) || index < 1) { + return; + } + map[index] = wrapper.dataset.answerRequired === '1'; + }); + + return map; + }, + + getRevealStateMap(): Record { + const root = this.$root ?? this.$el; + if (!root || !this.isRevealMode()) { + return {}; + } + + const wrappers = Array.from(root.querySelectorAll(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER)); + const map: Record = {}; + + wrappers.forEach((wrapper) => { + const index = Number(wrapper.getAttribute(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER_ATTR)); + if (Number.isNaN(index) || index < 1) { + return; + } + + const question = this.getQuestionElement(wrapper); + if (!question || question.getAttribute(QUIZ_REVEAL_CONFIG.DATA_REVEALED_ATTR) !== '1') { + return; + } + + const result = question.getAttribute(QUIZ_REVEAL_CONFIG.DATA_RESULT_ATTR); + if (result === QUIZ_REVEAL_CONFIG.DATA_OPTION_CORRECT) { + map[index] = 'correct'; + return; + } + if (result === QUIZ_REVEAL_CONFIG.DATA_OPTION_INCORRECT) { + map[index] = 'incorrect'; + } + }); + + return map; + }, + + getSkippedStateMap(): Record { + const root = this.$root ?? this.$el; + if (!root) { + return {}; + } + + const wrappers = Array.from(root.querySelectorAll(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER)); + const map: Record = {}; + + wrappers.forEach((wrapper) => { + const index = Number(wrapper.getAttribute(QUIZ_LAYOUT_SELECTORS.QUESTION_WRAPPER_ATTR)); + if (Number.isNaN(index) || index < 1) { + return; + } + map[index] = false; + }); + + return map; + }, + + markCurrentAsSkipped() { + const index = this.currentIndex; + if (!index || index < 1 || index > this.totalQuestions) { + return; + } + + if (this.revealStateByIndex[index]) { + this.skippedByIndex[index] = false; + return; + } + + const attempted = this.isQuestionAttempted(index); + this.skippedByIndex[index] = !attempted; + }, + scrollToQuestion() { const wrapper = this.getQuestionWrapper(this.currentIndex); if (!wrapper) { diff --git a/assets/src/js/frontend/learning-area/quiz/questions/matching.ts b/assets/src/js/frontend/learning-area/quiz/questions/matching.ts index fb8873efba..92e22635b4 100644 --- a/assets/src/js/frontend/learning-area/quiz/questions/matching.ts +++ b/assets/src/js/frontend/learning-area/quiz/questions/matching.ts @@ -95,6 +95,29 @@ const questionMatching = ( dropZoneEl.prepend(placeholder); }, + _animateDropSnap(dropZoneEl: HTMLElement, droppedOptionEl: HTMLElement) { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + return; + } + + dropZoneEl.animate([{ transform: 'scale(0.985)' }, { transform: 'scale(1.02)' }, { transform: 'scale(1)' }], { + duration: 220, + easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)', + }); + + droppedOptionEl.animate( + [ + { transform: 'scale(0.94)', opacity: 0.86 }, + { transform: 'scale(1.015)', opacity: 1 }, + { transform: 'scale(1)', opacity: 1 }, + ], + { + duration: 180, + easing: 'cubic-bezier(0.22, 1, 0.36, 1)', + }, + ); + }, + init() { if (!this.initialized) { this.setupDrag(); @@ -190,6 +213,10 @@ const questionMatching = ( const dropZoneEl = targetDropZone.element; const sourceId = operation.source.id; + if (!dropZoneEl) { + return; + } + const clone = document.createElement('div'); clone.setAttribute(QUESTION_MATCHING_CONSTANTS.ATTRS.OPTION, QUESTION_MATCHING_CONSTANTS.VALUES.DROPPED); clone.setAttribute(QUESTION_MATCHING_CONSTANTS.ATTRS.ID, String(sourceId)); @@ -205,6 +232,8 @@ const questionMatching = ( droppedOption.replaceWith(clone); } + this._animateDropSnap(dropZoneEl, clone); + this._matches[targetDropZone.id] = String(sourceId); const values = this._getValuesFromMatches(); this._callbacks.onDrop?.(values); diff --git a/assets/src/js/frontend/learning-area/quiz/submission.ts b/assets/src/js/frontend/learning-area/quiz/submission.ts index dc19af812e..798697d992 100644 --- a/assets/src/js/frontend/learning-area/quiz/submission.ts +++ b/assets/src/js/frontend/learning-area/quiz/submission.ts @@ -6,7 +6,6 @@ import type { AlpineComponentMeta } from '@Core/ts/types'; import { tutorConfig } from '@TutorShared/config/config'; import { wpAjaxInstance } from '@TutorShared/utils/api'; import endpoints from '@TutorShared/utils/endpoints'; -import { convertToFormData } from '@TutorShared/utils/form'; import { convertToErrorMessage } from '@TutorShared/utils/util'; import { @@ -54,7 +53,6 @@ const quizSubmission = (config: QuizSubmissionConfig) => { pendingNavigationUrl: '', beforeUnloadHandler: null as ((event: BeforeUnloadEvent) => string | void) | null, - pageHideHandler: null as (() => void) | null, navigationHandler: null as ((event: MouseEvent) => void) | null, $el: null as HTMLFormElement | null, @@ -82,16 +80,14 @@ const quizSubmission = (config: QuizSubmissionConfig) => { }) as EventListener); this.beforeUnloadHandler = this.handleBeforeUnload.bind(this); - this.pageHideHandler = this.handlePageHide.bind(this); this.navigationHandler = this.handleNavigationAttempt.bind(this); window.addEventListener('beforeunload', this.beforeUnloadHandler); - window.addEventListener('pagehide', this.pageHideHandler); document.addEventListener(QUIZ_ABANDON_CONFIG.NAVIGATION_EVENT, this.navigationHandler, true); this.submitQuizMutation = query.useMutation(this.submitQuizAttempt, { onSuccess: () => { - window.location.reload(); + this.performSafeReload(); }, onError: (error: Error) => { toast.error(convertToErrorMessage(error)); @@ -101,20 +97,16 @@ const quizSubmission = (config: QuizSubmissionConfig) => { this.abandonQuizMutation = query.useMutation(this.abandonQuizAttempt, { onSuccess: () => { this.isAbandoningNavigation = false; - this.skipBeforeUnload = true; - if (this.beforeUnloadHandler) { - window.removeEventListener('beforeunload', this.beforeUnloadHandler); - } if (this.pendingNavigationAction === 'navigate' && this.pendingNavigationUrl) { const nextUrl = this.pendingNavigationUrl; this.pendingNavigationAction = ''; this.pendingNavigationUrl = ''; - window.location.assign(nextUrl); + this.performSafeNavigate(nextUrl); return; } this.pendingNavigationAction = ''; this.pendingNavigationUrl = ''; - window.location.reload(); + this.performSafeReload(); }, onError: (error: Error) => { this.isAbandoningNavigation = false; @@ -125,7 +117,7 @@ const quizSubmission = (config: QuizSubmissionConfig) => { this.timeoutQuizMutation = query.useMutation(this.timeoutQuizAttempt, { onSuccess: () => { - window.location.reload(); + this.performSafeReload(); }, onError: (error: Error) => { toast.error(convertToErrorMessage(error)); @@ -248,10 +240,7 @@ const quizSubmission = (config: QuizSubmissionConfig) => { handleAbandonConfirm() { this.isAbandoningNavigation = true; - this.skipBeforeUnload = true; - if (this.beforeUnloadHandler) { - window.removeEventListener('beforeunload', this.beforeUnloadHandler); - } + this.prepareForNavigation(); if (!this.pendingNavigationAction) { this.pendingNavigationAction = 'reload'; } @@ -263,6 +252,7 @@ const quizSubmission = (config: QuizSubmissionConfig) => { this.pendingNavigationUrl = ''; this.isAbandoningNavigation = false; this.skipBeforeUnload = false; + this.beforeUnloadTriggered = false; }, handleNavigationAttempt(event: MouseEvent) { @@ -302,6 +292,7 @@ const quizSubmission = (config: QuizSubmissionConfig) => { handleBeforeUnload(event: BeforeUnloadEvent) { if (!this.shouldWarnOnUnload()) { + this.beforeUnloadTriggered = false; return; } this.beforeUnloadTriggered = true; @@ -310,25 +301,6 @@ const quizSubmission = (config: QuizSubmissionConfig) => { return ''; }, - handlePageHide() { - if (!this.beforeUnloadTriggered) { - return; - } - this.beforeUnloadTriggered = false; - - if (!this.formId || !form?.hasForm?.(this.formId)) { - return; - } - const data = form.getFormState?.(this.formId)?.values ?? {}; - const payload = this.buildSubmitPayload(data); - - if (!payload) { - return; - } - - this.sendAbandonBeacon(payload); - }, - shouldWarnOnUnload(): boolean { if (!this.formId || !form?.hasForm?.(this.formId)) { return false; @@ -348,27 +320,25 @@ const quizSubmission = (config: QuizSubmissionConfig) => { return true; }, - sendAbandonBeacon(payload: Record) { - const formData = convertToFormData( - { - action: endpoints.QUIZ_ABANDON, - [tutorConfig.nonce_key]: tutorConfig._tutor_nonce, - tutor_action: endpoints.QUIZ_ATTEMPT_SUBMIT, - ...payload, - }, - 'post', - ); - - if (navigator.sendBeacon) { - navigator.sendBeacon(tutorConfig.ajaxurl, formData); - return; + prepareForNavigation() { + this.skipBeforeUnload = true; + this.beforeUnloadTriggered = false; + if (this.beforeUnloadHandler) { + window.removeEventListener('beforeunload', this.beforeUnloadHandler); } + if (this.navigationHandler) { + document.removeEventListener(QUIZ_ABANDON_CONFIG.NAVIGATION_EVENT, this.navigationHandler, true); + } + }, + + performSafeReload() { + this.prepareForNavigation(); + window.location.reload(); + }, - fetch(tutorConfig.ajaxurl, { - method: 'POST', - body: formData, - keepalive: true, - }).catch(() => {}); + performSafeNavigate(url: string) { + this.prepareForNavigation(); + window.location.assign(url); }, handleQuizTimeoutAbandon() { diff --git a/assets/src/js/frontend/learning-area/quiz/timer.ts b/assets/src/js/frontend/learning-area/quiz/timer.ts index d82f6d9700..55d78b1066 100644 --- a/assets/src/js/frontend/learning-area/quiz/timer.ts +++ b/assets/src/js/frontend/learning-area/quiz/timer.ts @@ -1,16 +1,20 @@ import { TUTOR_CUSTOM_EVENTS } from '@Core/ts/constant'; +import { QUIZ_LAYOUT_KEYS } from './constants'; + const QUIZ_TIMER_CLASSES = { PROGRESS_ANIMATE: 'tutor-quiz-progress-animate', } as const; type QuizExpireAction = 'auto_submit' | 'auto_abandon' | 'autosubmit'; +type TimerState = 'initial' | 'warning' | 'critical'; interface QuizTimerConfig { duration: number; hasLimit?: boolean; expiresAction?: QuizExpireAction; formId?: string; + totalQuestions?: number; } /** @@ -30,6 +34,9 @@ const quizTimer = (config: QuizTimerConfig) => { expiresAction, formId: config.formId ?? '', timer: null as number | null, + shakeTimer: null as number | null, + shaking: false, + totalQuestions: Number(config.totalQuestions) || 0, $el: null as HTMLElement | null, init() { @@ -56,6 +63,7 @@ const quizTimer = (config: QuizTimerConfig) => { } }, 1000); + this.startShakeInterval(); this.$el?.classList.add(QUIZ_TIMER_CLASSES.PROGRESS_ANIMATE); }, @@ -65,6 +73,27 @@ const quizTimer = (config: QuizTimerConfig) => { this.timer = null; this.$el?.classList.remove(QUIZ_TIMER_CLASSES.PROGRESS_ANIMATE); } + this.stopShakeInterval(); + }, + + startShakeInterval() { + this.stopShakeInterval(); + this.shakeTimer = window.setInterval(() => { + if (this.timerState === 'critical') { + this.shaking = true; + window.setTimeout(() => { + this.shaking = false; + }, 500); + } + }, 2000); + }, + + stopShakeInterval() { + if (this.shakeTimer) { + clearInterval(this.shakeTimer); + this.shakeTimer = null; + } + this.shaking = false; }, normalizeExpireAction(action: QuizExpireAction) { @@ -122,6 +151,71 @@ const quizTimer = (config: QuizTimerConfig) => { return ((this.total - this.remaining) / this.total) * 100; }, + + get timerState(): TimerState { + if (!this.total || !this.hasLimit) { + return 'initial'; + } + + const remainingPercent = (this.remaining / this.total) * 100; + + if (remainingPercent <= 25) { + return 'critical'; + } + + if (remainingPercent <= 50) { + return 'warning'; + } + + return 'initial'; + }, + + get attemptedCount(): number { + const form = window.TutorCore?.form; + if (!form || !this.formId || !form.hasForm(this.formId)) { + return 0; + } + + const values = form.getFormState(this.formId).values ?? {}; + const questionIdsEntry = Object.entries(values).find(([key]) => key.includes('[quiz_question_ids]')); + + if (!questionIdsEntry) { + return 0; + } + + const questionIds = Array.isArray(questionIdsEntry[1]) ? questionIdsEntry[1] : []; + let count = 0; + + for (const id of questionIds) { + const needle = `${QUIZ_LAYOUT_KEYS.QUESTION_VALUE_PREFIX}[${id}]`; + const hasAnswer = Object.entries(values).some(([key, val]) => { + if (!key.includes(needle)) { + return false; + } + if (val === '' || val === null || val === undefined) { + return false; + } + if (Array.isArray(val) && val.length === 0) { + return false; + } + return true; + }); + + if (hasAnswer) { + count++; + } + } + + return count; + }, + + get attemptedProgress(): number { + if (!this.totalQuestions) { + return 0; + } + + return (this.attemptedCount / this.totalQuestions) * 100; + }, }; }; diff --git a/assets/src/scss/frontend/learning-area/components/quiz/_quiz.scss b/assets/src/scss/frontend/learning-area/components/quiz/_quiz.scss index b57da439ca..6a7b89f06d 100644 --- a/assets/src/scss/frontend/learning-area/components/quiz/_quiz.scss +++ b/assets/src/scss/frontend/learning-area/components/quiz/_quiz.scss @@ -1,17 +1,24 @@ @use '@Core/scss/mixins' as *; @use '@Core/scss/tokens' as *; -$tutor-quiz-header-offset: calc(44px + (#{$tutor-spacing-8} * 2)); -$tutor-quiz-header-offset-admin: calc(44px + (#{$tutor-spacing-8} * 2) + 46px); -$tutor-quiz-header-offset-admin-sm: calc(44px + (#{$tutor-spacing-8} * 2) + 32px); +$tutor-quiz-header-offset: calc((#{$tutor-spacing-8} * 2)); +$tutor-quiz-header-offset-admin: calc((#{$tutor-spacing-8} * 2) + 46px); +$tutor-quiz-header-offset-admin-sm: calc((#{$tutor-spacing-8} * 2) + 32px); $tutor-quiz-footer-offset: 96px; +$tutor-quiz-pagination-gap-from-footer: 32px; +$tutor-quiz-pagination-height: 48px; +$tutor-quiz-pagination-offset: calc(#{$tutor-quiz-footer-offset} + #{$tutor-quiz-pagination-gap-from-footer}); +$tutor-quiz-content-bottom-offset: calc( + #{$tutor-quiz-footer-offset} + #{$tutor-quiz-pagination-gap-from-footer} + #{$tutor-quiz-pagination-height} + + #{$tutor-spacing-2} +); .tutor-quiz { &-submission { @include tutor-flex(column); gap: $tutor-spacing-11; - height: 100%; - padding-top: calc((#{$tutor-spacing-8} * 2) + 44px + #{$tutor-spacing-14}); + height: fit-content; + padding-top: calc((#{$tutor-spacing-8} * 2) + #{$tutor-spacing-14}); background-color: $tutor-surface-base; @include tutor-breakpoint-down(sm) { @@ -22,15 +29,19 @@ $tutor-quiz-footer-offset: 96px; &[data-question-layout-view='question_pagination'] { max-height: 100dvh; padding-top: 0; + height: 100%; } &[data-question-layout-view='single_question'] .tutor-quiz-questions-pagination, &[data-question-layout-view='question_pagination'] .tutor-quiz-questions-pagination { - position: sticky; - top: $tutor-quiz-header-offset; + position: fixed; + left: 50%; + transform: translateX(-50%); + bottom: calc(#{$tutor-quiz-pagination-offset} + env(safe-area-inset-bottom, 0px)); z-index: $tutor-z-header; - background-color: $tutor-surface-base; - padding-block: $tutor-spacing-4; + background-color: transparent; + padding: 0; + margin-bottom: 0; } &[data-question-layout-view='single_question'] .tutor-quiz-footer, @@ -48,13 +59,13 @@ $tutor-quiz-footer-offset: 96px; &[data-question-layout-view='single_question'] .tutor-quiz-question-wrapper, &[data-question-layout-view='question_pagination'] .tutor-quiz-question-wrapper { - max-height: calc(100dvh - #{$tutor-quiz-header-offset} - #{$tutor-quiz-footer-offset}); - max-height: calc(100vh - #{$tutor-quiz-header-offset} - #{$tutor-quiz-footer-offset}); + max-height: calc(100dvh - #{$tutor-quiz-header-offset} - #{$tutor-quiz-content-bottom-offset}); + max-height: calc(100vh - #{$tutor-quiz-header-offset} - #{$tutor-quiz-content-bottom-offset}); display: flex; flex-direction: column; overflow: auto; scroll-margin-top: $tutor-quiz-header-offset; - padding-bottom: $tutor-quiz-footer-offset; + padding-bottom: $tutor-quiz-content-bottom-offset; } &[data-question-layout-view='single_question'] .tutor-quiz-question, @@ -67,27 +78,49 @@ $tutor-quiz-footer-offset: 96px; margin-inline-start: 0; } - body:has(#wpadminbar) & { - .tutor-quiz-questions-pagination { - top: $tutor-quiz-header-offset-admin; - } + &[data-question-layout-view='single_question'] .tutor-quiz-footer .tutor-quiz-footer-inner, + &[data-question-layout-view='question_pagination'] .tutor-quiz-footer .tutor-quiz-footer-inner { + justify-content: flex-end !important; + } + + &[data-question-layout-view='single_question'] + .tutor-quiz-footer + .tutor-quiz-footer-inner:has(.tutor-quiz-skip-btn:not([style*='display: none'])), + &[data-question-layout-view='question_pagination'] + .tutor-quiz-footer + .tutor-quiz-footer-inner:has(.tutor-quiz-skip-btn:not([style*='display: none'])), + &[data-question-layout-view='single_question'] + .tutor-quiz-footer + .tutor-quiz-footer-inner:has(.tutor-quiz-footer-feedback:not([style*='display: none'])), + &[data-question-layout-view='question_pagination'] + .tutor-quiz-footer + .tutor-quiz-footer-inner:has(.tutor-quiz-footer-feedback:not([style*='display: none'])) { + justify-content: space-between !important; + } + + &[data-question-layout-view='single_question'] .tutor-quiz-footer[data-reveal-state='correct'], + &[data-question-layout-view='question_pagination'] .tutor-quiz-footer[data-reveal-state='correct'] { + border-top-color: $tutor-border-success; + } + &[data-question-layout-view='single_question'] .tutor-quiz-footer[data-reveal-state='incorrect'], + &[data-question-layout-view='question_pagination'] .tutor-quiz-footer[data-reveal-state='incorrect'] { + border-top-color: $tutor-border-error; + } + + body:has(#wpadminbar) & { .tutor-quiz-question-wrapper { - max-height: calc(100dvh - #{$tutor-quiz-header-offset-admin} - #{$tutor-quiz-footer-offset}); - max-height: calc(100vh - #{$tutor-quiz-header-offset-admin} - #{$tutor-quiz-footer-offset}); + max-height: calc(100dvh - #{$tutor-quiz-header-offset-admin} - #{$tutor-quiz-content-bottom-offset}); + max-height: calc(100vh - #{$tutor-quiz-header-offset-admin} - #{$tutor-quiz-content-bottom-offset}); scroll-margin-top: $tutor-quiz-header-offset-admin; } } @include tutor-breakpoint-down(sm) { body:has(#wpadminbar) & { - .tutor-quiz-questions-pagination { - top: $tutor-quiz-header-offset-admin-sm; - } - .tutor-quiz-question-wrapper { - max-height: calc(100dvh - #{$tutor-quiz-header-offset-admin-sm} - #{$tutor-quiz-footer-offset}); - max-height: calc(100vh - #{$tutor-quiz-header-offset-admin-sm} - #{$tutor-quiz-footer-offset}); + max-height: calc(100dvh - #{$tutor-quiz-header-offset-admin-sm} - #{$tutor-quiz-content-bottom-offset}); + max-height: calc(100vh - #{$tutor-quiz-header-offset-admin-sm} - #{$tutor-quiz-content-bottom-offset}); scroll-margin-top: $tutor-quiz-header-offset-admin-sm; } @@ -120,7 +153,7 @@ $tutor-quiz-footer-offset: 96px; @include tutor-flex(column); @include tutor-container(792px); padding-inline: 0; - padding-block: $tutor-spacing-8; + padding-block: $tutor-spacing-5; gap: $tutor-spacing-5; @include tutor-breakpoint-down(sm) { @@ -129,13 +162,38 @@ $tutor-quiz-footer-offset: 96px; &-header { @include tutor-flex(row, center, space-between); + gap: $tutor-spacing-5; + + @include tutor-breakpoint-down(sm) { + align-items: flex-start; + } } - &-time { - @include tutor-flex(row, center, center); - @include tutor-typography('h3', 'semibold'); - gap: $tutor-spacing-2; - font-variant-numeric: tabular-nums; + &-meta { + @include tutor-flex(row, center, flex-start); + gap: $tutor-spacing-5; + min-width: 0; + flex: 1; + + @include tutor-breakpoint-down(sm) { + align-items: flex-start; + flex-direction: column; + gap: $tutor-spacing-3; + } + } + + &-title { + @include tutor-typography('h5', 'semibold'); + color: $tutor-text-primary; + padding-top: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + } + + &-bar-wrapper { + width: 100%; } &-animate { @@ -146,6 +204,96 @@ $tutor-quiz-footer-offset: 96px; } } + &-timer-frame { + @include tutor-flex(row, center, center); + position: relative; + display: inline-flex; + flex-shrink: 0; + @include tutor-transition(color); + + &.is-initial { + color: $tutor-text-brand; + } + + &.is-warning { + color: $tutor-text-exception4; + } + + &.is-critical { + color: $tutor-text-critical; + } + + &[data-shaking='1'] { + animation: tutor-quiz-timer-shake 0.5s ease-in-out; + } + + svg { + display: block; + } + } + + &-timer-text { + @include tutor-typography('h5', 'semibold'); + @include tutor-flex-center; + position: absolute; + inset-inline: 0; + bottom: 0; + height: 27px; + line-height: 1; + font-variant-numeric: tabular-nums; + @include tutor-transition(color); + + &.is-initial { + color: $tutor-text-brand; + } + + &.is-warning { + color: $tutor-text-exception4; + } + + &.is-critical { + color: $tutor-text-critical; + } + } + + &-timer-separator { + line-height: 1; + margin-bottom: $tutor-spacing-1; + } + + &-timer-digit-wrapper { + height: 1.25em; + overflow: hidden; + line-height: 1.25em; + } + + &-timer-reel { + @include tutor-flex-column; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + span { + display: block; + height: 1.25em; + text-align: center; + } + } + + @keyframes tutor-quiz-timer-shake { + 0%, + 100% { + transform: rotate(0deg); + } + 10%, + 50%, + 90% { + transform: rotate(-6deg); + } + 30%, + 70% { + transform: rotate(6deg); + } + } + &-questions { @include tutor-flex(column); @include tutor-container(792px); @@ -166,9 +314,30 @@ $tutor-quiz-footer-offset: 96px; } } + &-question-meta { + @include tutor-flex(row, center, space-between); + @include tutor-container(792px); + padding-inline: 0; + + @include tutor-breakpoint-down(sm) { + padding-inline: $tutor-spacing-6; + } + } + + &-question-indicator, + &-attempt-progress { + @include tutor-typography('small'); + + strong { + @include tutor-typography('small', 'bold'); + } + } + &-questions-pagination { @include tutor-flex(row, center, center); - margin-bottom: $tutor-spacing-5; + width: max-content; + max-width: calc(100vw - #{$tutor-spacing-8}); + overflow: visible; ul { @include tutor-flex(row, center, center); @@ -188,32 +357,287 @@ $tutor-quiz-footer-offset: 96px; background-color: $tutor-surface-l1; color: $tutor-text-secondary; cursor: pointer; - transition: - background-color 0.2s ease, - border-color 0.2s ease, - color 0.2s ease; + @include tutor-transition((background-color, border-color, color, transform)); + + .tutor-quiz-question-paginate-icon { + display: none; + } &:hover { border-color: $tutor-border-hover; background-color: $tutor-surface-l1-hover; } + } - &.active { - border-color: $tutor-border-brand; - background-color: $tutor-surface-brand-secondary; + &[data-pagination-style='shape'] { + ul { + gap: $tutor-spacing-2; + } + + .tutor-quiz-question-paginate-item { + width: 8px; + min-width: 8px; + height: 24px; + border: 0; + border-radius: $tutor-radius-full; + background-color: $tutor-icon-brand-secondary; + color: transparent; + position: relative; + padding: 0; + + .tutor-quiz-question-paginate-label, + .tutor-quiz-question-paginate-icon { + display: none; + } + + &.answered { + background-color: $tutor-icon-brand; + } + + &.correct { + background-color: $tutor-icon-success-primary; + } + + &.incorrect { + background-color: $tutor-icon-critical; + } + + &.active { + background-color: $tutor-icon-brand-hover; + + &::before, + &::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-radius: $tutor-radius-2xl; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + } + + &::before { + top: -10px; + border-top: 7px solid $tutor-icon-brand-hover; + } + + &::after { + bottom: -10px; + border-bottom: 7px solid $tutor-icon-brand-hover; + } + + &.correct { + background-color: $tutor-icon-success-primary; + + &::before { + border-top-color: $tutor-icon-success-primary; + } + + &::after { + border-bottom-color: $tutor-icon-success-primary; + } + } + + &.incorrect { + background-color: $tutor-icon-critical; + + &::before { + border-top-color: $tutor-icon-critical; + } + + &::after { + border-bottom-color: $tutor-icon-critical; + } + } + } + } + } + + &[data-pagination-style='radio'] { + ul { + gap: $tutor-spacing-4; + } + + .tutor-quiz-question-paginate-item { + @include tutor-flex(row, center, center); + width: 16px; + min-width: 16px; + height: 16px; + border-radius: $tutor-radius-full; + border: 1px solid transparent; + background-color: $tutor-icon-brand-secondary; + color: $tutor-surface-l1; + position: relative; + padding: 0; + + .tutor-quiz-question-paginate-label { + display: none; + } + + .tutor-quiz-question-paginate-icon { + display: none; + width: 12px; + height: 12px; + } + + &.correct { + background-color: $tutor-icon-success-primary; + color: $tutor-surface-l1; + box-shadow: none; + + .tutor-quiz-question-paginate-icon-correct { + display: inline-flex; + } + } + + &.incorrect { + background-color: $tutor-icon-critical; + color: $tutor-surface-l1; + box-shadow: none; + + .tutor-quiz-question-paginate-icon-incorrect { + display: inline-flex; + } + } + + &.skipped { + background-color: $tutor-icon-disabled; + color: $tutor-text-subdued; + } + + &.active { + background-color: $tutor-icon-brand-hover; + border-color: $tutor-icon-brand-hover; + box-shadow: 0 0 0 2px $tutor-surface-l1 inset; + + &.correct { + background-color: $tutor-icon-success-primary; + color: $tutor-text-light; + border-color: $tutor-icon-success-primary; + } + + &.incorrect { + background-color: $tutor-icon-critical; + color: $tutor-text-light; + border-color: $tutor-icon-critical; + } + } + } + } + + &[data-pagination-style='number'] { + ul { + flex-wrap: nowrap; + max-width: 100%; + height: 28px; + overflow: visible; + background-color: $tutor-surface-l1; + border: 1px solid $tutor-border-idle; + border-radius: $tutor-radius-full; + padding: $tutor-spacing-1 $tutor-spacing-3; + gap: $tutor-spacing-2; + } + + .tutor-quiz-question-paginate-item { + @include tutor-flex(row, center, center); + @include tutor-typography('small', 'medium'); + width: 24px; + min-width: 24px; + height: 38px; + border-radius: $tutor-radius-full; + border-color: transparent; + background-color: transparent; color: $tutor-text-brand; + + .tutor-quiz-question-paginate-icon { + display: none; + } + + .tutor-quiz-question-paginate-label { + color: inherit; + } + + &:hover:not(.active) { + color: $tutor-text-brand-hover; + background-color: $tutor-icon-brand-secondary; + + &.correct { + color: $tutor-text-success; + background-color: $tutor-surface-success; + } + + &.incorrect { + color: $tutor-text-critical; + background-color: $tutor-surface-critical; + } + + &.skipped { + color: $tutor-text-secondary; + background-color: transparent; + } + } + + &.correct { + color: $tutor-text-success; + } + + &.incorrect { + color: $tutor-text-critical; + } + + &.skipped { + color: $tutor-text-subdued; + } + + &.active { + background-color: $tutor-icon-brand; + color: $tutor-text-primary-inverse; + + .tutor-quiz-question-paginate-label { + font-weight: $tutor-font-weight-bold; + } + + &.correct { + background-color: $tutor-icon-success-primary; + color: $tutor-text-light; + } + + &.incorrect { + background-color: $tutor-icon-critical; + color: $tutor-text-light; + } + } } } } &-question-wrapper { width: 100%; + + &-active { + view-transition-name: tutor-quiz-question; + } } &-skip-btn { margin-inline-start: auto; } + &-answer-next-btn, + &-previous-btn, + &-submit-btn { + padding-inline: $tutor-spacing-9; + + &:disabled { + background-color: $tutor-button-disabled; + border-color: $tutor-button-disabled; + color: $tutor-text-subdued; + opacity: 1; + } + } + &-question { @include tutor-flex(column); gap: $tutor-spacing-7; @@ -642,6 +1066,11 @@ $tutor-quiz-footer-offset: 96px; background-color: $tutor-surface-l1; padding: 0 $tutor-spacing-4; + // Hide dnd-kit's default drop-back animation frame for matching draggables. + &[data-dnd-dragging][data-dnd-dropping] { + opacity: 0 !important; + } + [data-title] { @include tutor-typography('small'); @include tutor-flex(row, center, flex-start); @@ -760,6 +1189,32 @@ $tutor-quiz-footer-offset: 96px; margin-top: auto; border-top: 1px solid $tutor-border-idle; + &[data-reveal-state='correct'] { + border-top-color: $tutor-border-success; + + .tutor-quiz-footer-feedback-icon { + background-color: $tutor-surface-exception3-highlight; + color: $tutor-icon-success-primary; + } + + .tutor-quiz-footer-feedback-text { + color: $tutor-text-success; + } + } + + &[data-reveal-state='incorrect'] { + border-top-color: $tutor-border-error; + + .tutor-quiz-footer-feedback-icon { + background-color: $tutor-surface-critical; + color: $tutor-icon-critical; + } + + .tutor-quiz-footer-feedback-text { + color: $tutor-text-critical; + } + } + &-inner { @include tutor-flex(row, center, flex-start); gap: $tutor-spacing-4; @@ -768,17 +1223,32 @@ $tutor-quiz-footer-offset: 96px; margin-inline: auto; } - &[data-position='first'] &-inner { - justify-content: flex-end; + &-feedback { + @include tutor-flex(row, center, flex-start); + gap: $tutor-spacing-3; + margin-inline-end: auto; } - &[data-position='only'] &-inner { - justify-content: center; + &-feedback-icon { + @include tutor-flex(row, center, center); + width: 40px; + height: 40px; + border-radius: $tutor-radius-full; + background-color: $tutor-surface-l1; + + svg { + display: block; + } } - &[data-position='last'] &-inner, - &[data-position='middle'] &-inner { - justify-content: space-between; + &-feedback-text { + @include tutor-typography('h6', 'medium'); + } + + &-actions { + @include tutor-flex(row, center, flex-end); + gap: $tutor-spacing-6; + margin-inline-start: auto; } } diff --git a/classes/Icon.php b/classes/Icon.php index 1199152057..210ae05493 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -94,6 +94,7 @@ final class Icon { const CIRCUM_LOCK = 'circum-lock'; const CLOCK = 'clock'; const CLOCK_2 = 'clock-2'; + const CLOCK_FRAME = 'clock-frame'; const CODING = 'coding'; const COLLAPSED = 'collapsed'; const COLOR_OPTION = 'color-option'; diff --git a/classes/Utils.php b/classes/Utils.php index a99ccf84e6..f86be8f8e1 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -10422,6 +10422,29 @@ public function allowed_profile_bio_tags( $tags = array() ) { return wp_parse_args( $tags, $supported_tags ); } + /** + * Get allowed basic inline tags, useful while using wp_kses. + * + * @since 4.0.0 + * + * @param array $tags additional tags. + * + * @return array + */ + public function allowed_basic_inline_tags( array $tags = array() ): array { + $defaults = array( + 'br' => array(), + 'b' => array(), + 'em' => array(), + 'i' => array(), + 'span' => array(), + 'strong' => array(), + 'u' => array(), + ); + + return wp_parse_args( $tags, $defaults ); + } + /** * Get allowed tags for avatar, useful while using wp_kses * diff --git a/components/ConfirmationModal.php b/components/ConfirmationModal.php index f6dbb0151a..5da3adad87 100644 --- a/components/ConfirmationModal.php +++ b/components/ConfirmationModal.php @@ -412,7 +412,7 @@ class="tutor-btn tutor-btn-secondary tutor-btn-small" } $this->component_string = sprintf( - '
+ '