@dereekb/dbx-form 13.3.1 → 13.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, inject, input, effect, computed, Component, model } from '@angular/core';
|
|
3
|
+
import { ComponentStore } from '@ngrx/component-store';
|
|
4
|
+
import { asArray, range } from '@dereekb/util';
|
|
5
|
+
import { map, combineLatest, distinctUntilChanged, shareReplay, switchMap, of, first } from 'rxjs';
|
|
6
|
+
import { asObservable } from '@dereekb/rxjs';
|
|
7
|
+
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
|
8
|
+
import * as i1$1 from '@dereekb/dbx-core';
|
|
9
|
+
import { DbxInjectionComponent } from '@dereekb/dbx-core';
|
|
10
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
11
|
+
import * as i1 from '@dereekb/dbx-web';
|
|
12
|
+
import { DbxButtonModule, DbxWindowKeyDownListenerDirective, DbxActionModule } from '@dereekb/dbx-web';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* NgRx ComponentStore that manages quiz lifecycle: question navigation, answer tracking,
|
|
16
|
+
* submission state, and navigation locking.
|
|
17
|
+
*
|
|
18
|
+
* Provided at the component level by `QuizComponent`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Access from a child component via DI:
|
|
23
|
+
* readonly quizStore = inject(QuizStore);
|
|
24
|
+
* this.quizStore.startQuiz();
|
|
25
|
+
* this.quizStore.updateAnswerForCurrentQuestion(5);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
class QuizStore extends ComponentStore {
|
|
29
|
+
constructor() {
|
|
30
|
+
super({
|
|
31
|
+
quiz: undefined,
|
|
32
|
+
startedQuiz: false,
|
|
33
|
+
submittedQuiz: false,
|
|
34
|
+
answers: new Map(),
|
|
35
|
+
questionIndex: undefined,
|
|
36
|
+
unansweredQuestions: [],
|
|
37
|
+
completedQuestions: [],
|
|
38
|
+
questionMap: undefined,
|
|
39
|
+
autoAdvanceToNextQuestion: true,
|
|
40
|
+
allowVisitingPreviousQuestion: true,
|
|
41
|
+
allowSkipQuestion: false
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
quiz$ = this.select((state) => state.quiz);
|
|
45
|
+
titleDetails$ = this.quiz$.pipe(map((quiz) => quiz?.titleDetails));
|
|
46
|
+
questions$ = this.quiz$.pipe(map((quiz) => quiz?.questions ?? []));
|
|
47
|
+
startedQuiz$ = this.select((state) => state.startedQuiz);
|
|
48
|
+
lockQuizNavigation$ = this.select((state) => state.lockQuizNavigation);
|
|
49
|
+
submittedQuiz$ = this.select((state) => state.submittedQuiz);
|
|
50
|
+
answers$ = this.select((state) => state.answers);
|
|
51
|
+
questionIndex$ = this.select((state) => state.questionIndex ?? 0);
|
|
52
|
+
completedQuestions$ = this.select((state) => state.completedQuestions);
|
|
53
|
+
unansweredQuestions$ = this.select((state) => state.unansweredQuestions);
|
|
54
|
+
hasAnswerForEachQuestion$ = this.select((state) => state.unansweredQuestions.length === 0);
|
|
55
|
+
isAtEndOfQuestions$ = this.select((state) => (state.questionIndex ?? 0) >= (state.quiz?.questions.length ?? 0));
|
|
56
|
+
canGoToPreviousQuestion$ = this.select((state) => !state.lockQuizNavigation && state.allowVisitingPreviousQuestion && state.questionIndex != null && state.questionIndex > 0);
|
|
57
|
+
canGoToNextQuestion$ = this.select((state) => {
|
|
58
|
+
const newQuestionIndex = computeAdvanceIndexOnState(state, 1);
|
|
59
|
+
return !state.lockQuizNavigation && state.questionIndex != null && newQuestionIndex != null && newQuestionIndex > state.questionIndex;
|
|
60
|
+
});
|
|
61
|
+
currentQuestion$ = combineLatest([this.questions$, this.questionIndex$]).pipe(map(([questions, questionIndex]) => {
|
|
62
|
+
const question = questions[questionIndex];
|
|
63
|
+
let result;
|
|
64
|
+
if (question) {
|
|
65
|
+
result = {
|
|
66
|
+
...question,
|
|
67
|
+
index: questionIndex
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}), distinctUntilChanged(), shareReplay(1));
|
|
72
|
+
/**
|
|
73
|
+
* Returns a reactive observable of the answer for a given question, looked up by id, index, or the current question.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* // By current question:
|
|
78
|
+
* store.answerForQuestion({ currentIndex: true }).subscribe(answer => console.log(answer));
|
|
79
|
+
* // By question id:
|
|
80
|
+
* store.answerForQuestion({ id: 'q1' }).subscribe(answer => console.log(answer));
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
answerForQuestion(lookupInput) {
|
|
84
|
+
return asObservable(lookupInput).pipe(switchMap((lookup) => {
|
|
85
|
+
const { id, index, currentIndex } = lookup;
|
|
86
|
+
let result;
|
|
87
|
+
if (currentIndex) {
|
|
88
|
+
result = this.currentQuestion$.pipe(switchMap((question) => this.answerForQuestion({ index: question?.index })));
|
|
89
|
+
}
|
|
90
|
+
else if (id != null) {
|
|
91
|
+
result = this.answers$.pipe(map((answers) => answers.get(id)));
|
|
92
|
+
}
|
|
93
|
+
else if (index != null) {
|
|
94
|
+
result = this.questions$.pipe(switchMap((questions) => {
|
|
95
|
+
const question = questions[index];
|
|
96
|
+
let result;
|
|
97
|
+
if (question) {
|
|
98
|
+
result = this.answerForQuestion({ id: question.id });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
result = of(undefined);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
result = of(undefined);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}), distinctUntilChanged(), shareReplay(1));
|
|
111
|
+
}
|
|
112
|
+
startQuiz = this.updater((state) => startQuizOnState(state));
|
|
113
|
+
setQuiz = this.updater((state, quiz) => setQuizOnState(state, quiz));
|
|
114
|
+
/**
|
|
115
|
+
* Resets the quiz entirely, back to the pre-quiz state.
|
|
116
|
+
*/
|
|
117
|
+
resetQuiz = this.updater((state) => resetQuizOnState(state));
|
|
118
|
+
/**
|
|
119
|
+
* Restarts the quiz to the first question.
|
|
120
|
+
*/
|
|
121
|
+
restartQuizToFirstQuestion = this.updater((state) => restartQuizToFirstQuestionOnState(state));
|
|
122
|
+
setAnswers = this.updater((state, answers) => setAnswersOnState(state, answers));
|
|
123
|
+
updateAnswers = this.updater((state, answers) => updateAnswersOnState(state, answers));
|
|
124
|
+
updateAnswerForCurrentQuestion = this.updater((state, answerData) => updateAnswerForCurrentQuestionOnState(state, answerData));
|
|
125
|
+
setQuestionIndex = this.updater((state, questionIndex) => ({ ...state, questionIndex }));
|
|
126
|
+
setAutoAdvanceToNextQuestion = this.updater((state, autoAdvanceToNextQuestion) => ({ ...state, autoAdvanceToNextQuestion }));
|
|
127
|
+
setAllowSkipQuestion = this.updater((state, allowSkipQuestion) => ({ ...state, allowSkipQuestion }));
|
|
128
|
+
setAllowVisitingPreviousQuestion = this.updater((state, allowVisitingPreviousQuestion) => ({ ...state, allowVisitingPreviousQuestion }));
|
|
129
|
+
goToNextQuestion = this.updater((state) => advanceQuestionOnState(state, 1));
|
|
130
|
+
goToPreviousQuestion = this.updater((state) => advanceQuestionOnState(state, -1));
|
|
131
|
+
setLockQuizNavigation = this.updater((state, lockQuizNavigation) => ({ ...state, lockQuizNavigation }));
|
|
132
|
+
setSubmittedQuiz = this.updater((state, submittedQuiz) => ({ ...state, submittedQuiz }));
|
|
133
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
134
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizStore });
|
|
135
|
+
}
|
|
136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizStore, decorators: [{
|
|
137
|
+
type: Injectable
|
|
138
|
+
}], ctorParameters: () => [] });
|
|
139
|
+
function computeAdvanceIndexOnState(state, advancement) {
|
|
140
|
+
const { questionIndex, allowSkipQuestion, unansweredQuestions, lockQuizNavigation } = state;
|
|
141
|
+
const maxQuestionIndex = state.quiz?.questions.length;
|
|
142
|
+
let newQuestionIndex;
|
|
143
|
+
if (maxQuestionIndex != null && questionIndex != null && !lockQuizNavigation) {
|
|
144
|
+
let maxAllowedIndex = maxQuestionIndex;
|
|
145
|
+
if (!allowSkipQuestion) {
|
|
146
|
+
maxAllowedIndex = unansweredQuestions[0]?.index ?? maxQuestionIndex;
|
|
147
|
+
}
|
|
148
|
+
newQuestionIndex = Math.max(0, Math.min(maxAllowedIndex, questionIndex + advancement));
|
|
149
|
+
}
|
|
150
|
+
return newQuestionIndex;
|
|
151
|
+
}
|
|
152
|
+
function advanceQuestionOnState(state, advancement) {
|
|
153
|
+
const newQuestionIndex = computeAdvanceIndexOnState(state, advancement);
|
|
154
|
+
let nextState = state;
|
|
155
|
+
if (newQuestionIndex != null) {
|
|
156
|
+
nextState = {
|
|
157
|
+
...state,
|
|
158
|
+
questionIndex: newQuestionIndex
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return nextState;
|
|
162
|
+
}
|
|
163
|
+
function startQuizOnState(state) {
|
|
164
|
+
const { startedQuiz } = state;
|
|
165
|
+
let nextState = state;
|
|
166
|
+
if (!startedQuiz) {
|
|
167
|
+
nextState = {
|
|
168
|
+
...state,
|
|
169
|
+
startedQuiz: true,
|
|
170
|
+
submittedQuiz: false,
|
|
171
|
+
lockQuizNavigation: false,
|
|
172
|
+
questionIndex: 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return nextState;
|
|
176
|
+
}
|
|
177
|
+
function resetQuizOnState(state) {
|
|
178
|
+
return {
|
|
179
|
+
...restartQuizToFirstQuestionOnState(state),
|
|
180
|
+
startedQuiz: false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function restartQuizToFirstQuestionOnState(state) {
|
|
184
|
+
return setAnswersOnState(startQuizOnState({
|
|
185
|
+
...state,
|
|
186
|
+
startedQuiz: false
|
|
187
|
+
}), []);
|
|
188
|
+
}
|
|
189
|
+
function setQuizOnState(state, quiz) {
|
|
190
|
+
let questionMap = undefined;
|
|
191
|
+
const currentAnswers = Array.from(state.answers.values());
|
|
192
|
+
if (quiz?.questions) {
|
|
193
|
+
questionMap = new Map(quiz.questions.map((question) => [question.id, question]));
|
|
194
|
+
}
|
|
195
|
+
return setAnswersOnState({ ...state, quiz, questionMap }, currentAnswers);
|
|
196
|
+
}
|
|
197
|
+
function setAnswersOnState(state, newAnswers) {
|
|
198
|
+
return updateAnswersOnState({
|
|
199
|
+
...state,
|
|
200
|
+
answers: new Map()
|
|
201
|
+
}, newAnswers);
|
|
202
|
+
}
|
|
203
|
+
function updateAnswerForCurrentQuestionOnState(state, answerData) {
|
|
204
|
+
const { questionIndex } = state;
|
|
205
|
+
let nextState = state;
|
|
206
|
+
if (questionIndex != null) {
|
|
207
|
+
const currentQuestion = state.quiz?.questions[questionIndex];
|
|
208
|
+
if (currentQuestion) {
|
|
209
|
+
const answer = {
|
|
210
|
+
id: currentQuestion.id,
|
|
211
|
+
data: answerData
|
|
212
|
+
};
|
|
213
|
+
nextState = updateAnswersOnState(state, [answer]);
|
|
214
|
+
if (state.autoAdvanceToNextQuestion) {
|
|
215
|
+
nextState.questionIndex = questionIndex + 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return nextState;
|
|
220
|
+
}
|
|
221
|
+
function updateAnswersOnState(state, inputAnswers) {
|
|
222
|
+
const { quiz, answers: currentAnswers } = state;
|
|
223
|
+
const answers = new Map(currentAnswers);
|
|
224
|
+
asArray(inputAnswers).forEach((answer) => {
|
|
225
|
+
answers.set(answer.id, answer);
|
|
226
|
+
});
|
|
227
|
+
const completedQuestions = [];
|
|
228
|
+
const unansweredQuestions = [];
|
|
229
|
+
if (quiz?.questions) {
|
|
230
|
+
quiz.questions.forEach((question, index) => {
|
|
231
|
+
if (answers.has(question.id)) {
|
|
232
|
+
completedQuestions.push({ id: question.id, index });
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
unansweredQuestions.push({ id: question.id, index });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return { ...state, unansweredQuestions, completedQuestions, answers };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Abstract accessor injected into answer/question child components to read the current question
|
|
244
|
+
* and write answers back to the QuizStore without coupling to it directly.
|
|
245
|
+
*
|
|
246
|
+
* Use `provideCurrentQuestionQuizQuestionAccessor()` to bind this to the store's current question.
|
|
247
|
+
*/
|
|
248
|
+
class QuizQuestionAccessor {
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Provides QuizQuestionAccessor bound to the current question in QuizStore.
|
|
252
|
+
*
|
|
253
|
+
* @usage
|
|
254
|
+
* ```typescript
|
|
255
|
+
* @Component({
|
|
256
|
+
* providers: [QuizStore, provideCurrentQuestionQuizQuestionAccessor()]
|
|
257
|
+
* })
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
function provideCurrentQuestionQuizQuestionAccessor() {
|
|
261
|
+
return {
|
|
262
|
+
provide: QuizQuestionAccessor,
|
|
263
|
+
useFactory: (quizStore) => {
|
|
264
|
+
return {
|
|
265
|
+
quiz$: quizStore.quiz$,
|
|
266
|
+
question$: quizStore.currentQuestion$,
|
|
267
|
+
answer$: quizStore.answerForQuestion({ currentIndex: true }),
|
|
268
|
+
setAnswer: (answer) => quizStore.updateAnswerForCurrentQuestion(answer),
|
|
269
|
+
setAnswerSource: (answer) => quizStore.updateAnswerForCurrentQuestion(answer)
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
deps: [QuizStore]
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Top-level quiz container that orchestrates pre-quiz, active quiz, and post-quiz views.
|
|
278
|
+
*
|
|
279
|
+
* Provides its own `QuizStore` and `QuizQuestionAccessor`, so child components injected via
|
|
280
|
+
* `DbxInjectionComponent` can access quiz state directly through DI.
|
|
281
|
+
*
|
|
282
|
+
* Supports keyboard navigation: Enter (start / next), ArrowLeft (previous), ArrowRight (next).
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```html
|
|
286
|
+
* <dbx-quiz [quiz]="myQuiz"></dbx-quiz>
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
class QuizComponent {
|
|
290
|
+
quizStore = inject(QuizStore);
|
|
291
|
+
quiz = input.required(...(ngDevMode ? [{ debugName: "quiz" }] : []));
|
|
292
|
+
keysFilter = ['Enter', 'ArrowLeft', 'ArrowRight'];
|
|
293
|
+
quizEffect = effect(() => {
|
|
294
|
+
const quiz = this.quiz();
|
|
295
|
+
this.quizStore.setQuiz(quiz);
|
|
296
|
+
}, { ...(ngDevMode ? { debugName: "quizEffect" } : {}), allowSignalWrites: true });
|
|
297
|
+
quiz$ = toObservable(this.quiz);
|
|
298
|
+
quizTitleSignal = computed(() => this.quiz()?.titleDetails.title, ...(ngDevMode ? [{ debugName: "quizTitleSignal" }] : []));
|
|
299
|
+
currentQuestionSignal = toSignal(this.quizStore.currentQuestion$);
|
|
300
|
+
questionTitleSignal = computed(() => {
|
|
301
|
+
const currentQuestion = this.currentQuestionSignal();
|
|
302
|
+
return currentQuestion ? `Question ${currentQuestion.index + 1}` : '';
|
|
303
|
+
}, ...(ngDevMode ? [{ debugName: "questionTitleSignal" }] : []));
|
|
304
|
+
startedQuiz$ = this.quizStore.startedQuiz$;
|
|
305
|
+
currentQuestion$ = this.quizStore.currentQuestion$;
|
|
306
|
+
canGoToPreviousQuestionSignal = toSignal(this.quizStore.canGoToPreviousQuestion$, { initialValue: false });
|
|
307
|
+
canGoToNextQuestionSignal = toSignal(this.quizStore.canGoToNextQuestion$, { initialValue: false });
|
|
308
|
+
viewConfig$ = this.startedQuiz$.pipe(switchMap((started) => {
|
|
309
|
+
if (!started) {
|
|
310
|
+
return this.quiz$.pipe(map((quiz) => {
|
|
311
|
+
const viewConfig = {
|
|
312
|
+
state: 'pre-quiz',
|
|
313
|
+
preQuizComponent: quiz?.preQuizComponentConfig
|
|
314
|
+
};
|
|
315
|
+
return viewConfig;
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
return combineLatest([this.quiz$, this.currentQuestion$, this.quizStore.isAtEndOfQuestions$]).pipe(map(([quiz, currentQuestion, isAtEndOfQuestions]) => {
|
|
320
|
+
let viewConfig;
|
|
321
|
+
if (isAtEndOfQuestions) {
|
|
322
|
+
viewConfig = {
|
|
323
|
+
state: 'post-quiz',
|
|
324
|
+
resultsComponent: quiz?.resultsComponentConfig
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
viewConfig = {
|
|
329
|
+
state: 'quiz',
|
|
330
|
+
questionComponent: currentQuestion?.questionComponentConfig,
|
|
331
|
+
answerComponent: currentQuestion?.answerComponentConfig
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return viewConfig;
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
338
|
+
viewConfigSignal = toSignal(this.viewConfig$, { initialValue: { state: 'init' } });
|
|
339
|
+
viewStateSignal = computed(() => this.viewConfigSignal()?.state ?? 'init', ...(ngDevMode ? [{ debugName: "viewStateSignal" }] : []));
|
|
340
|
+
preQuizComponentConfigSignal = computed(() => this.viewConfigSignal()?.preQuizComponent, ...(ngDevMode ? [{ debugName: "preQuizComponentConfigSignal" }] : []));
|
|
341
|
+
questionComponentConfigSignal = computed(() => this.viewConfigSignal()?.questionComponent, ...(ngDevMode ? [{ debugName: "questionComponentConfigSignal" }] : []));
|
|
342
|
+
answerComponentConfigSignal = computed(() => this.viewConfigSignal()?.answerComponent, ...(ngDevMode ? [{ debugName: "answerComponentConfigSignal" }] : []));
|
|
343
|
+
resultsComponentConfigSignal = computed(() => this.viewConfigSignal()?.resultsComponent, ...(ngDevMode ? [{ debugName: "resultsComponentConfigSignal" }] : []));
|
|
344
|
+
handleKeyDown(event) {
|
|
345
|
+
const code = event.code;
|
|
346
|
+
switch (code) {
|
|
347
|
+
case 'Enter':
|
|
348
|
+
this.quizStore.startedQuiz$.pipe(first()).subscribe((started) => {
|
|
349
|
+
if (!started) {
|
|
350
|
+
this.quizStore.startQuiz();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.clickNextQuestion();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
case 'ArrowLeft':
|
|
358
|
+
this.clickPreviousQuestion();
|
|
359
|
+
break;
|
|
360
|
+
case 'ArrowRight':
|
|
361
|
+
this.clickNextQuestion();
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
clickPreviousQuestion() {
|
|
366
|
+
this.quizStore.goToPreviousQuestion();
|
|
367
|
+
}
|
|
368
|
+
clickNextQuestion() {
|
|
369
|
+
this.quizStore.goToNextQuestion();
|
|
370
|
+
}
|
|
371
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
372
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: QuizComponent, isStandalone: true, selector: "dbx-quiz", inputs: { quiz: { classPropertyName: "quiz", publicName: "quiz", isSignal: true, isRequired: true, transformFunction: null } }, providers: [QuizStore, provideCurrentQuestionQuizQuestionAccessor()], ngImport: i0, template: "<div class=\"dbx-quiz\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"keysFilter\">\n @switch (viewStateSignal()) {\n @case ('pre-quiz') {\n <ng-container *ngTemplateOutlet=\"preQuizTemplate\"></ng-container>\n }\n @case ('quiz') {\n <ng-container *ngTemplateOutlet=\"quizTemplate\"></ng-container>\n }\n @case ('post-quiz') {\n <ng-container *ngTemplateOutlet=\"postQuizTemplate\"></ng-container>\n }\n }\n</div>\n\n<!-- Pre-Quiz -->\n<ng-template #preQuizTemplate>\n <div class=\"dbx-quiz-pre-quiz\">\n <dbx-injection [config]=\"preQuizComponentConfigSignal()\"></dbx-injection>\n </div>\n</ng-template>\n\n<!-- Quiz -->\n<ng-template #quizTemplate>\n <div class=\"dbx-quiz-quiz\">\n <ng-container *ngTemplateOutlet=\"headerTemplate\"></ng-container>\n <div class=\"dbx-quiz-question dbx-pb3\">\n <h4 class=\"dbx-quiz-question-title\">{{ questionTitleSignal() }}</h4>\n <dbx-injection [config]=\"questionComponentConfigSignal()\"></dbx-injection>\n </div>\n <div class=\"dbx-quiz-answer dbx-pt3\">\n <dbx-injection [config]=\"answerComponentConfigSignal()\"></dbx-injection>\n </div>\n </div>\n</ng-template>\n\n<!-- Post-Quiz -->\n<ng-template #postQuizTemplate>\n <div class=\"dbx-quiz-post-quiz\">\n <ng-container *ngTemplateOutlet=\"headerTemplate\"></ng-container>\n <dbx-injection [config]=\"resultsComponentConfigSignal()\"></dbx-injection>\n </div>\n</ng-template>\n\n<!-- Header Template -->\n<ng-template #headerTemplate>\n <div class=\"dbx-quiz-header\">\n <div class=\"dbx-flex-group\">\n <dbx-button [disabled]=\"!canGoToPreviousQuestionSignal()\" icon=\"arrow_back\" (buttonClick)=\"clickPreviousQuestion()\"></dbx-button>\n <span class=\"spacer\"></span>\n <span class=\"mat-h2\">{{ quizTitleSignal() }}</span>\n <span class=\"spacer\"></span>\n <dbx-button [disabled]=\"!canGoToNextQuestionSignal()\" icon=\"arrow_forward\" (buttonClick)=\"clickNextQuestion()\"></dbx-button>\n </div>\n </div>\n</ng-template>\n", dependencies: [{ kind: "component", type: DbxInjectionComponent, selector: "dbx-injection, [dbxInjection], [dbx-injection]", inputs: ["config", "template"] }, { kind: "ngmodule", type: DbxButtonModule }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }, { kind: "directive", type: DbxWindowKeyDownListenerDirective, selector: "[dbxWindowKeyDownListener]", inputs: ["dbxWindowKeyDownEnabled", "dbxWindowKeyDownFilter"], outputs: ["dbxWindowKeyDownListener"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
373
|
+
}
|
|
374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizComponent, decorators: [{
|
|
375
|
+
type: Component,
|
|
376
|
+
args: [{ selector: 'dbx-quiz', imports: [DbxInjectionComponent, DbxButtonModule, DbxWindowKeyDownListenerDirective, NgTemplateOutlet], providers: [QuizStore, provideCurrentQuestionQuizQuestionAccessor()], standalone: true, template: "<div class=\"dbx-quiz\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"keysFilter\">\n @switch (viewStateSignal()) {\n @case ('pre-quiz') {\n <ng-container *ngTemplateOutlet=\"preQuizTemplate\"></ng-container>\n }\n @case ('quiz') {\n <ng-container *ngTemplateOutlet=\"quizTemplate\"></ng-container>\n }\n @case ('post-quiz') {\n <ng-container *ngTemplateOutlet=\"postQuizTemplate\"></ng-container>\n }\n }\n</div>\n\n<!-- Pre-Quiz -->\n<ng-template #preQuizTemplate>\n <div class=\"dbx-quiz-pre-quiz\">\n <dbx-injection [config]=\"preQuizComponentConfigSignal()\"></dbx-injection>\n </div>\n</ng-template>\n\n<!-- Quiz -->\n<ng-template #quizTemplate>\n <div class=\"dbx-quiz-quiz\">\n <ng-container *ngTemplateOutlet=\"headerTemplate\"></ng-container>\n <div class=\"dbx-quiz-question dbx-pb3\">\n <h4 class=\"dbx-quiz-question-title\">{{ questionTitleSignal() }}</h4>\n <dbx-injection [config]=\"questionComponentConfigSignal()\"></dbx-injection>\n </div>\n <div class=\"dbx-quiz-answer dbx-pt3\">\n <dbx-injection [config]=\"answerComponentConfigSignal()\"></dbx-injection>\n </div>\n </div>\n</ng-template>\n\n<!-- Post-Quiz -->\n<ng-template #postQuizTemplate>\n <div class=\"dbx-quiz-post-quiz\">\n <ng-container *ngTemplateOutlet=\"headerTemplate\"></ng-container>\n <dbx-injection [config]=\"resultsComponentConfigSignal()\"></dbx-injection>\n </div>\n</ng-template>\n\n<!-- Header Template -->\n<ng-template #headerTemplate>\n <div class=\"dbx-quiz-header\">\n <div class=\"dbx-flex-group\">\n <dbx-button [disabled]=\"!canGoToPreviousQuestionSignal()\" icon=\"arrow_back\" (buttonClick)=\"clickPreviousQuestion()\"></dbx-button>\n <span class=\"spacer\"></span>\n <span class=\"mat-h2\">{{ quizTitleSignal() }}</span>\n <span class=\"spacer\"></span>\n <dbx-button [disabled]=\"!canGoToNextQuestionSignal()\" icon=\"arrow_forward\" (buttonClick)=\"clickNextQuestion()\"></dbx-button>\n </div>\n </div>\n</ng-template>\n" }]
|
|
377
|
+
}], propDecorators: { quiz: [{ type: i0.Input, args: [{ isSignal: true, alias: "quiz", required: true }] }] } });
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Answer component that displays configurable number buttons.
|
|
381
|
+
*
|
|
382
|
+
* @usage
|
|
383
|
+
* Used as an answer component in a QuizQuestion's answerComponentConfig.
|
|
384
|
+
* Defaults to 1-5 range if no config is provided.
|
|
385
|
+
*
|
|
386
|
+
* ```typescript
|
|
387
|
+
* answerComponentConfig: {
|
|
388
|
+
* componentClass: QuizAnswerNumberComponent,
|
|
389
|
+
* init: (instance: QuizAnswerNumberComponent) => {
|
|
390
|
+
* instance.config.set({ range: { start: 1, end: 11 } });
|
|
391
|
+
* }
|
|
392
|
+
* }
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
class QuizAnswerNumberComponent {
|
|
396
|
+
questionAccessor = inject(QuizQuestionAccessor);
|
|
397
|
+
config = model(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
|
|
398
|
+
currentAnswerSignal = toSignal(this.questionAccessor.answer$);
|
|
399
|
+
currentAnswerValueSignal = computed(() => this.currentAnswerSignal()?.data, ...(ngDevMode ? [{ debugName: "currentAnswerValueSignal" }] : []));
|
|
400
|
+
choicesSignal = computed(() => {
|
|
401
|
+
const { range: inputRange, numbers: inputNumbers, preset } = this.config() ?? { preset: 'oneToFive' };
|
|
402
|
+
const currentAnswer = this.currentAnswerValueSignal();
|
|
403
|
+
let useRange;
|
|
404
|
+
let useNumbers;
|
|
405
|
+
if (preset) {
|
|
406
|
+
switch (preset) {
|
|
407
|
+
case 'oneToFive':
|
|
408
|
+
default:
|
|
409
|
+
useRange = { start: 1, end: 6 };
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else if (inputRange) {
|
|
414
|
+
useRange = inputRange;
|
|
415
|
+
}
|
|
416
|
+
else if (inputNumbers) {
|
|
417
|
+
useNumbers = inputNumbers;
|
|
418
|
+
}
|
|
419
|
+
let numbers;
|
|
420
|
+
if (useRange) {
|
|
421
|
+
numbers = range(useRange);
|
|
422
|
+
}
|
|
423
|
+
else if (useNumbers) {
|
|
424
|
+
numbers = useNumbers ?? [];
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
numbers = [];
|
|
428
|
+
}
|
|
429
|
+
const choices = numbers.map((number) => {
|
|
430
|
+
return {
|
|
431
|
+
number,
|
|
432
|
+
selected: currentAnswer === number
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
return choices;
|
|
436
|
+
}, ...(ngDevMode ? [{ debugName: "choicesSignal" }] : []));
|
|
437
|
+
relevantKeysSignal = computed(() => {
|
|
438
|
+
const choices = this.choicesSignal();
|
|
439
|
+
return choices.map((choice) => choice.number.toString());
|
|
440
|
+
}, ...(ngDevMode ? [{ debugName: "relevantKeysSignal" }] : []));
|
|
441
|
+
clickedAnswer(answer) {
|
|
442
|
+
this.questionAccessor.setAnswer(answer);
|
|
443
|
+
}
|
|
444
|
+
handleKeyDown(event) {
|
|
445
|
+
const number = Number(event.key);
|
|
446
|
+
if (!isNaN(number)) {
|
|
447
|
+
this.clickedAnswer(number);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizAnswerNumberComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
451
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: QuizAnswerNumberComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { config: "configChange" }, ngImport: i0, template: "<div class=\"dbx-quiz-answer-number\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"relevantKeysSignal()\">\n <div class=\"dbx-quiz-answer-number-buttons dbx-button-wrap-group\">\n @for (choice of choicesSignal(); track choice.number) {\n <dbx-button [color]=\"choice.selected ? 'accent' : 'primary'\" [raised]=\"true\" (buttonClick)=\"clickedAnswer(choice.number)\">{{ choice.number }}</dbx-button>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: DbxButtonModule }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }, { kind: "directive", type: DbxWindowKeyDownListenerDirective, selector: "[dbxWindowKeyDownListener]", inputs: ["dbxWindowKeyDownEnabled", "dbxWindowKeyDownFilter"], outputs: ["dbxWindowKeyDownListener"] }] });
|
|
452
|
+
}
|
|
453
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizAnswerNumberComponent, decorators: [{
|
|
454
|
+
type: Component,
|
|
455
|
+
args: [{ imports: [DbxButtonModule, DbxWindowKeyDownListenerDirective], standalone: true, template: "<div class=\"dbx-quiz-answer-number\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"relevantKeysSignal()\">\n <div class=\"dbx-quiz-answer-number-buttons dbx-button-wrap-group\">\n @for (choice of choicesSignal(); track choice.number) {\n <dbx-button [color]=\"choice.selected ? 'accent' : 'primary'\" [raised]=\"true\" (buttonClick)=\"clickedAnswer(choice.number)\">{{ choice.number }}</dbx-button>\n }\n </div>\n</div>\n" }]
|
|
456
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }, { type: i0.Output, args: ["configChange"] }] } });
|
|
457
|
+
|
|
458
|
+
const MULTIPLE_CHOICE_LETTERS = `abcdefghijklmnopqrstuvwxyz`;
|
|
459
|
+
/**
|
|
460
|
+
* Answer component that displays multiple choice letter-labeled buttons.
|
|
461
|
+
*
|
|
462
|
+
* @usage
|
|
463
|
+
* Used as an answer component in a QuizQuestion's answerComponentConfig.
|
|
464
|
+
* Supports keyboard shortcuts (pressing the letter key selects that answer).
|
|
465
|
+
*
|
|
466
|
+
* ```typescript
|
|
467
|
+
* answerComponentConfig: {
|
|
468
|
+
* componentClass: QuizAnswerMultipleChoiceComponent,
|
|
469
|
+
* init: (instance: QuizAnswerMultipleChoiceComponent) => {
|
|
470
|
+
* instance.config.set({
|
|
471
|
+
* answerText: ['Option A', 'Option B', 'Option C'],
|
|
472
|
+
* correctAnswerIndex: 1
|
|
473
|
+
* });
|
|
474
|
+
* }
|
|
475
|
+
* }
|
|
476
|
+
* ```
|
|
477
|
+
*/
|
|
478
|
+
class QuizAnswerMultipleChoiceComponent {
|
|
479
|
+
questionAccessor = inject(QuizQuestionAccessor);
|
|
480
|
+
config = model(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
|
|
481
|
+
currentAnswerSignal = toSignal(this.questionAccessor.answer$);
|
|
482
|
+
currentAnswerValueSignal = computed(() => this.currentAnswerSignal()?.data, ...(ngDevMode ? [{ debugName: "currentAnswerValueSignal" }] : []));
|
|
483
|
+
choicesSignal = computed(() => {
|
|
484
|
+
const config = this.config();
|
|
485
|
+
const currentAnswer = this.currentAnswerValueSignal();
|
|
486
|
+
const answers = config?.answerText ?? [];
|
|
487
|
+
const correctAnswerIndex = config?.correctAnswerIndex;
|
|
488
|
+
const choices = answers.map((text, i) => {
|
|
489
|
+
const letter = MULTIPLE_CHOICE_LETTERS[i];
|
|
490
|
+
return {
|
|
491
|
+
letter: MULTIPLE_CHOICE_LETTERS[i].toUpperCase(),
|
|
492
|
+
text,
|
|
493
|
+
selected: currentAnswer?.letter === letter,
|
|
494
|
+
isCorrectAnswer: correctAnswerIndex === i
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
return choices;
|
|
498
|
+
}, ...(ngDevMode ? [{ debugName: "choicesSignal" }] : []));
|
|
499
|
+
relevantKeysSignal = computed(() => {
|
|
500
|
+
const answersCount = this.config()?.answerText.length ?? 0;
|
|
501
|
+
const relevantKeys = [];
|
|
502
|
+
const numbersRange = answersCount > 0 ? range(1, answersCount + 1) : [];
|
|
503
|
+
for (const number of numbersRange) {
|
|
504
|
+
const answerLetter = MULTIPLE_CHOICE_LETTERS[number - 1];
|
|
505
|
+
relevantKeys.push(answerLetter);
|
|
506
|
+
}
|
|
507
|
+
return relevantKeys;
|
|
508
|
+
}, ...(ngDevMode ? [{ debugName: "relevantKeysSignal" }] : []));
|
|
509
|
+
clickedAnswer(answer) {
|
|
510
|
+
this.questionAccessor.setAnswer(answer);
|
|
511
|
+
}
|
|
512
|
+
handleKeyDown(event) {
|
|
513
|
+
if (event.key.length === 1) {
|
|
514
|
+
const choices = this.choicesSignal();
|
|
515
|
+
const selectedLetter = event.key.toUpperCase();
|
|
516
|
+
const choice = choices.find((x) => x.letter === selectedLetter);
|
|
517
|
+
if (choice) {
|
|
518
|
+
this.clickedAnswer(choice);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizAnswerMultipleChoiceComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
523
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: QuizAnswerMultipleChoiceComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { config: "configChange" }, ngImport: i0, template: "<div class=\"dbx-quiz-answer-multiplechoice\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"relevantKeysSignal()\">\n <div class=\"dbx-quiz-answer-multiplechoice-buttons dbx-button-column dbx-button-wide\">\n @for (choice of choicesSignal(); track choice.letter) {\n <dbx-button class=\"dbx-w100\" [color]=\"choice.selected ? 'accent' : 'primary'\" [raised]=\"true\" (buttonClick)=\"clickedAnswer(choice)\">\n <div class=\"dbx-quiz-answer-multiplechoice-button-content\">\n <span class=\"dbx-quiz-answer-multiplechoice-button-letter\">{{ choice.letter }})</span>\n <span class=\"dbx-quiz-answer-multiplechoice-button-text\">{{ choice.text }}</span>\n </div>\n </dbx-button>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: DbxButtonModule }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }, { kind: "directive", type: DbxWindowKeyDownListenerDirective, selector: "[dbxWindowKeyDownListener]", inputs: ["dbxWindowKeyDownEnabled", "dbxWindowKeyDownFilter"], outputs: ["dbxWindowKeyDownListener"] }] });
|
|
524
|
+
}
|
|
525
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizAnswerMultipleChoiceComponent, decorators: [{
|
|
526
|
+
type: Component,
|
|
527
|
+
args: [{ imports: [DbxButtonModule, DbxWindowKeyDownListenerDirective], standalone: true, template: "<div class=\"dbx-quiz-answer-multiplechoice\" (dbxWindowKeyDownListener)=\"handleKeyDown($event)\" [dbxWindowKeyDownFilter]=\"relevantKeysSignal()\">\n <div class=\"dbx-quiz-answer-multiplechoice-buttons dbx-button-column dbx-button-wide\">\n @for (choice of choicesSignal(); track choice.letter) {\n <dbx-button class=\"dbx-w100\" [color]=\"choice.selected ? 'accent' : 'primary'\" [raised]=\"true\" (buttonClick)=\"clickedAnswer(choice)\">\n <div class=\"dbx-quiz-answer-multiplechoice-button-content\">\n <span class=\"dbx-quiz-answer-multiplechoice-button-letter\">{{ choice.letter }})</span>\n <span class=\"dbx-quiz-answer-multiplechoice-button-text\">{{ choice.text }}</span>\n </div>\n </dbx-button>\n }\n </div>\n</div>\n" }]
|
|
528
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }, { type: i0.Output, args: ["configChange"] }] } });
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Pre-quiz intro component that displays the quiz title, subtitle, description, and a start button.
|
|
532
|
+
*
|
|
533
|
+
* @usage
|
|
534
|
+
* Used as a preQuizComponentConfig in a Quiz definition.
|
|
535
|
+
* Inherits title details from the quiz unless overridden via config.
|
|
536
|
+
*
|
|
537
|
+
* ```typescript
|
|
538
|
+
* preQuizComponentConfig: {
|
|
539
|
+
* componentClass: QuizPreQuizIntroComponent,
|
|
540
|
+
* init: (instance: QuizPreQuizIntroComponent) => {
|
|
541
|
+
* instance.config.set({ subtitle: 'Custom subtitle' });
|
|
542
|
+
* }
|
|
543
|
+
* }
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
class QuizPreQuizIntroComponent {
|
|
547
|
+
quizStore = inject(QuizStore);
|
|
548
|
+
config = model(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
|
|
549
|
+
quizTitleDetailsSignal = toSignal(this.quizStore.titleDetails$);
|
|
550
|
+
configSignal = computed(() => {
|
|
551
|
+
const config = this.config();
|
|
552
|
+
const titleDetails = this.quizTitleDetailsSignal();
|
|
553
|
+
return {
|
|
554
|
+
title: config?.title ?? titleDetails?.title,
|
|
555
|
+
subtitle: config?.subtitle ?? titleDetails?.subtitle,
|
|
556
|
+
description: config?.description ?? titleDetails?.description
|
|
557
|
+
};
|
|
558
|
+
}, ...(ngDevMode ? [{ debugName: "configSignal" }] : []));
|
|
559
|
+
titleSignal = computed(() => this.configSignal()?.title, ...(ngDevMode ? [{ debugName: "titleSignal" }] : []));
|
|
560
|
+
subtitleSignal = computed(() => this.configSignal()?.subtitle, ...(ngDevMode ? [{ debugName: "subtitleSignal" }] : []));
|
|
561
|
+
descriptionSignal = computed(() => this.configSignal()?.description, ...(ngDevMode ? [{ debugName: "descriptionSignal" }] : []));
|
|
562
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizPreQuizIntroComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
563
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: QuizPreQuizIntroComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { config: "configChange" }, ngImport: i0, template: "<div>\n <div>\n <h1>{{ titleSignal() }}</h1>\n <h2>{{ subtitleSignal() }}</h2>\n <p>{{ descriptionSignal() }}</p>\n </div>\n <div>\n <dbx-button [raised]=\"true\" (buttonClick)=\"quizStore.startQuiz()\" text=\"Start Quiz\"></dbx-button>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: DbxButtonModule }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }] });
|
|
564
|
+
}
|
|
565
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizPreQuizIntroComponent, decorators: [{
|
|
566
|
+
type: Component,
|
|
567
|
+
args: [{ imports: [DbxButtonModule], standalone: true, template: "<div>\n <div>\n <h1>{{ titleSignal() }}</h1>\n <h2>{{ subtitleSignal() }}</h2>\n <p>{{ descriptionSignal() }}</p>\n </div>\n <div>\n <dbx-button [raised]=\"true\" (buttonClick)=\"quizStore.startQuiz()\" text=\"Start Quiz\"></dbx-button>\n </div>\n</div>\n" }]
|
|
568
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }, { type: i0.Output, args: ["configChange"] }] } });
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Question component that displays text with optional prompt and guidance.
|
|
572
|
+
*
|
|
573
|
+
* @usage
|
|
574
|
+
* Used as a questionComponentConfig in a QuizQuestion definition.
|
|
575
|
+
*
|
|
576
|
+
* ```typescript
|
|
577
|
+
* questionComponentConfig: {
|
|
578
|
+
* componentClass: QuizQuestionTextComponent,
|
|
579
|
+
* init: (instance: QuizQuestionTextComponent) => {
|
|
580
|
+
* instance.config.set({ text: 'How do you handle ambiguity?', prompt: 'Rate yourself:', guidance: '1=Never, 5=Always' });
|
|
581
|
+
* }
|
|
582
|
+
* }
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
class QuizQuestionTextComponent {
|
|
586
|
+
config = model(...(ngDevMode ? [undefined, { debugName: "config" }] : []));
|
|
587
|
+
promptSignal = computed(() => this.config()?.prompt, ...(ngDevMode ? [{ debugName: "promptSignal" }] : []));
|
|
588
|
+
textSignal = computed(() => this.config()?.text, ...(ngDevMode ? [{ debugName: "textSignal" }] : []));
|
|
589
|
+
guidanceSignal = computed(() => this.config()?.guidance, ...(ngDevMode ? [{ debugName: "guidanceSignal" }] : []));
|
|
590
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizQuestionTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
591
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: QuizQuestionTextComponent, isStandalone: true, selector: "ng-component", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { config: "configChange" }, ngImport: i0, template: "<div class=\"dbx-quiz-question-text\">\n <div class=\"dbx-quiz-question-text-content\">\n @if (promptSignal() !== null) {\n <div class=\"dbx-hint dbx-pb3\">{{ promptSignal() }}</div>\n }\n <div class=\"dbx-pb3\">{{ textSignal() }}</div>\n @if (guidanceSignal() !== null) {\n <div class=\"dbx-small dbx-hint\">{{ guidanceSignal() }}</div>\n }\n </div>\n</div>\n" });
|
|
592
|
+
}
|
|
593
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: QuizQuestionTextComponent, decorators: [{
|
|
594
|
+
type: Component,
|
|
595
|
+
args: [{ standalone: true, template: "<div class=\"dbx-quiz-question-text\">\n <div class=\"dbx-quiz-question-text-content\">\n @if (promptSignal() !== null) {\n <div class=\"dbx-hint dbx-pb3\">{{ promptSignal() }}</div>\n }\n <div class=\"dbx-pb3\">{{ textSignal() }}</div>\n @if (guidanceSignal() !== null) {\n <div class=\"dbx-small dbx-hint\">{{ guidanceSignal() }}</div>\n }\n </div>\n</div>\n" }]
|
|
596
|
+
}], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }, { type: i0.Output, args: ["configChange"] }] } });
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Post-quiz component that handles quiz submission and displays pre/post submit content.
|
|
600
|
+
*
|
|
601
|
+
* @usage
|
|
602
|
+
* Use as a wrapper in your results component template:
|
|
603
|
+
*
|
|
604
|
+
* ```html
|
|
605
|
+
* <dbx-quiz-post-quiz [handleSubmitQuiz]="handleSubmitQuiz">
|
|
606
|
+
* <div presubmit>Pre-submit content...</div>
|
|
607
|
+
* <div postsubmit>Post-submit content (scores, etc.)...</div>
|
|
608
|
+
* </dbx-quiz-post-quiz>
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
class DbxQuizPostQuizComponent {
|
|
612
|
+
quizStore = inject(QuizStore);
|
|
613
|
+
quizSubmittedSignal = toSignal(this.quizStore.submittedQuiz$);
|
|
614
|
+
stateSignal = computed(() => {
|
|
615
|
+
const submitted = this.quizSubmittedSignal();
|
|
616
|
+
if (submitted) {
|
|
617
|
+
return 'postsubmit';
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
return 'presubmit';
|
|
621
|
+
}
|
|
622
|
+
}, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
|
|
623
|
+
handleSubmitQuiz = input(...(ngDevMode ? [undefined, { debugName: "handleSubmitQuiz" }] : []));
|
|
624
|
+
handleSubmitQuizButton = (_, context) => {
|
|
625
|
+
this.quizStore.setLockQuizNavigation(true);
|
|
626
|
+
const handler = this.handleSubmitQuiz();
|
|
627
|
+
if (handler) {
|
|
628
|
+
return handler(_, context);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
context.reject();
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
handleSubmitQuizSuccess = () => {
|
|
635
|
+
this.quizStore.setSubmittedQuiz(true);
|
|
636
|
+
};
|
|
637
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizPostQuizComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
638
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DbxQuizPostQuizComponent, isStandalone: true, selector: "dbx-quiz-post-quiz", inputs: { handleSubmitQuiz: { classPropertyName: "handleSubmitQuiz", publicName: "handleSubmitQuiz", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"dbx-post-quiz text-center\">\n <h3>Quiz Completed</h3>\n <div>\n <ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n @if (stateSignal() === 'presubmit') {\n <ng-container *ngTemplateOutlet=\"preSubmitTemplate\"></ng-container>\n } @else {\n <ng-container *ngTemplateOutlet=\"postSubmitTemplate\"></ng-container>\n }\n </div>\n</div>\n\n<!-- Pre-Submit -->\n<ng-template #preSubmitTemplate>\n <ng-content select=\"[presubmit]\"></ng-content>\n @if (handleSubmitQuiz()) {\n <div class=\"dbx-post-quiz-submit\">\n <div dbxAction dbxActionLogger dbxActionValue dbxActionSnackbarError [dbxActionHandler]=\"handleSubmitQuizButton\" [dbxActionSuccessHandler]=\"handleSubmitQuizSuccess\">\n <dbx-button [disabled]=\"quizSubmittedSignal()\" [raised]=\"true\" dbxActionButton>Submit Quiz</dbx-button>\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Post-Submit -->\n<ng-template #postSubmitTemplate>\n <ng-content select=\"[postsubmit]\"></ng-content>\n</ng-template>\n\n<!-- Content -->\n<ng-template #contentTemplate>\n <ng-content></ng-content>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: DbxButtonModule }, { kind: "directive", type: i1$1.DbxActionButtonDirective, selector: "[dbxActionButton]" }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }, { kind: "ngmodule", type: DbxActionModule }, { kind: "directive", type: i1$1.DbxActionDirective, selector: "dbx-action,[dbxAction]", exportAs: ["action", "dbxAction"] }, { kind: "directive", type: i1$1.DbxActionHandlerDirective, selector: "[dbxActionHandler]", inputs: ["dbxActionHandler"] }, { kind: "directive", type: i1$1.DbxActionValueDirective, selector: "dbxActionValue,[dbxActionValue]", inputs: ["dbxActionValue"] }, { kind: "directive", type: i1$1.DbxActionContextLoggerDirective, selector: "[dbxActionLogger],[dbxActionContextLogger]" }, { kind: "directive", type: i1$1.DbxActionSuccessHandlerDirective, selector: "[dbxActionSuccessHandler]", inputs: ["dbxActionSuccessHandler"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
639
|
+
}
|
|
640
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizPostQuizComponent, decorators: [{
|
|
641
|
+
type: Component,
|
|
642
|
+
args: [{ selector: 'dbx-quiz-post-quiz', imports: [DbxButtonModule, DbxActionModule, NgTemplateOutlet], standalone: true, template: "<div class=\"dbx-post-quiz text-center\">\n <h3>Quiz Completed</h3>\n <div>\n <ng-container *ngTemplateOutlet=\"contentTemplate\"></ng-container>\n @if (stateSignal() === 'presubmit') {\n <ng-container *ngTemplateOutlet=\"preSubmitTemplate\"></ng-container>\n } @else {\n <ng-container *ngTemplateOutlet=\"postSubmitTemplate\"></ng-container>\n }\n </div>\n</div>\n\n<!-- Pre-Submit -->\n<ng-template #preSubmitTemplate>\n <ng-content select=\"[presubmit]\"></ng-content>\n @if (handleSubmitQuiz()) {\n <div class=\"dbx-post-quiz-submit\">\n <div dbxAction dbxActionLogger dbxActionValue dbxActionSnackbarError [dbxActionHandler]=\"handleSubmitQuizButton\" [dbxActionSuccessHandler]=\"handleSubmitQuizSuccess\">\n <dbx-button [disabled]=\"quizSubmittedSignal()\" [raised]=\"true\" dbxActionButton>Submit Quiz</dbx-button>\n </div>\n </div>\n }\n</ng-template>\n\n<!-- Post-Submit -->\n<ng-template #postSubmitTemplate>\n <ng-content select=\"[postsubmit]\"></ng-content>\n</ng-template>\n\n<!-- Content -->\n<ng-template #contentTemplate>\n <ng-content></ng-content>\n</ng-template>\n" }]
|
|
643
|
+
}], propDecorators: { handleSubmitQuiz: [{ type: i0.Input, args: [{ isSignal: true, alias: "handleSubmitQuiz", required: false }] }] } });
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Button component that restarts the quiz to the first question.
|
|
647
|
+
*
|
|
648
|
+
* @usage
|
|
649
|
+
* ```html
|
|
650
|
+
* <dbx-quiz-reset-button buttonText="Try Again"></dbx-quiz-reset-button>
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
class DbxQuizResetButtonComponent {
|
|
654
|
+
quizStore = inject(QuizStore);
|
|
655
|
+
buttonText = input(`Restart Quiz`, ...(ngDevMode ? [{ debugName: "buttonText" }] : []));
|
|
656
|
+
handleResetQuizButton = (_, context) => {
|
|
657
|
+
this.quizStore.restartQuizToFirstQuestion();
|
|
658
|
+
context.success();
|
|
659
|
+
};
|
|
660
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizResetButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
661
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.0", type: DbxQuizResetButtonComponent, isStandalone: true, selector: "dbx-quiz-reset-button", inputs: { buttonText: { classPropertyName: "buttonText", publicName: "buttonText", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
662
|
+
<div class="dbx-quiz-reset-button">
|
|
663
|
+
<div dbxAction dbxActionLogger dbxActionValue dbxActionSnackbarError [dbxActionHandler]="handleResetQuizButton">
|
|
664
|
+
<dbx-button [raised]="true" [text]="buttonText()" dbxActionButton></dbx-button>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: DbxButtonModule }, { kind: "directive", type: i1$1.DbxActionButtonDirective, selector: "[dbxActionButton]" }, { kind: "component", type: i1.DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "mode"] }, { kind: "ngmodule", type: DbxActionModule }, { kind: "directive", type: i1$1.DbxActionDirective, selector: "dbx-action,[dbxAction]", exportAs: ["action", "dbxAction"] }, { kind: "directive", type: i1$1.DbxActionHandlerDirective, selector: "[dbxActionHandler]", inputs: ["dbxActionHandler"] }, { kind: "directive", type: i1$1.DbxActionValueDirective, selector: "dbxActionValue,[dbxActionValue]", inputs: ["dbxActionValue"] }, { kind: "directive", type: i1$1.DbxActionContextLoggerDirective, selector: "[dbxActionLogger],[dbxActionContextLogger]" }] });
|
|
668
|
+
}
|
|
669
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizResetButtonComponent, decorators: [{
|
|
670
|
+
type: Component,
|
|
671
|
+
args: [{
|
|
672
|
+
selector: 'dbx-quiz-reset-button',
|
|
673
|
+
template: `
|
|
674
|
+
<div class="dbx-quiz-reset-button">
|
|
675
|
+
<div dbxAction dbxActionLogger dbxActionValue dbxActionSnackbarError [dbxActionHandler]="handleResetQuizButton">
|
|
676
|
+
<dbx-button [raised]="true" [text]="buttonText()" dbxActionButton></dbx-button>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
`,
|
|
680
|
+
imports: [DbxButtonModule, DbxActionModule],
|
|
681
|
+
standalone: true
|
|
682
|
+
}]
|
|
683
|
+
}], propDecorators: { buttonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonText", required: false }] }] } });
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Generic quiz score display component.
|
|
687
|
+
*
|
|
688
|
+
* @usage
|
|
689
|
+
* ```html
|
|
690
|
+
* <dbx-quiz-score [input]="scoreInput"></dbx-quiz-score>
|
|
691
|
+
* ```
|
|
692
|
+
*/
|
|
693
|
+
class DbxQuizScoreComponent {
|
|
694
|
+
input = input(...(ngDevMode ? [undefined, { debugName: "input" }] : []));
|
|
695
|
+
scoreSignal = computed(() => this.input()?.score, ...(ngDevMode ? [{ debugName: "scoreSignal" }] : []));
|
|
696
|
+
maxScoreSignal = computed(() => this.input()?.maxScore, ...(ngDevMode ? [{ debugName: "maxScoreSignal" }] : []));
|
|
697
|
+
feedbackTextSignal = computed(() => this.input()?.feedbackText ?? '', ...(ngDevMode ? [{ debugName: "feedbackTextSignal" }] : []));
|
|
698
|
+
subtitleSignal = computed(() => this.input()?.subtitle, ...(ngDevMode ? [{ debugName: "subtitleSignal" }] : []));
|
|
699
|
+
showRetakeButtonSignal = computed(() => this.input()?.showRetakeButton, ...(ngDevMode ? [{ debugName: "showRetakeButtonSignal" }] : []));
|
|
700
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizScoreComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
701
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DbxQuizScoreComponent, isStandalone: true, selector: "dbx-quiz-score", inputs: { input: { classPropertyName: "input", publicName: "input", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
702
|
+
<div class="dbx-quiz-score">
|
|
703
|
+
<h3 class="dbx-quiz-score-score">{{ scoreSignal() }}/{{ maxScoreSignal() }}</h3>
|
|
704
|
+
<p class="dbx-quiz-score-text">{{ feedbackTextSignal() }}</p>
|
|
705
|
+
@if (subtitleSignal()) {
|
|
706
|
+
<p class="dbx-quiz-score-subtitle">{{ subtitleSignal() }}</p>
|
|
707
|
+
}
|
|
708
|
+
@if (showRetakeButtonSignal()) {
|
|
709
|
+
<dbx-quiz-reset-button buttonText="Retake Quiz"></dbx-quiz-reset-button>
|
|
710
|
+
}
|
|
711
|
+
</div>
|
|
712
|
+
`, isInline: true, dependencies: [{ kind: "component", type: DbxQuizResetButtonComponent, selector: "dbx-quiz-reset-button", inputs: ["buttonText"] }] });
|
|
713
|
+
}
|
|
714
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DbxQuizScoreComponent, decorators: [{
|
|
715
|
+
type: Component,
|
|
716
|
+
args: [{
|
|
717
|
+
selector: 'dbx-quiz-score',
|
|
718
|
+
template: `
|
|
719
|
+
<div class="dbx-quiz-score">
|
|
720
|
+
<h3 class="dbx-quiz-score-score">{{ scoreSignal() }}/{{ maxScoreSignal() }}</h3>
|
|
721
|
+
<p class="dbx-quiz-score-text">{{ feedbackTextSignal() }}</p>
|
|
722
|
+
@if (subtitleSignal()) {
|
|
723
|
+
<p class="dbx-quiz-score-subtitle">{{ subtitleSignal() }}</p>
|
|
724
|
+
}
|
|
725
|
+
@if (showRetakeButtonSignal()) {
|
|
726
|
+
<dbx-quiz-reset-button buttonText="Retake Quiz"></dbx-quiz-reset-button>
|
|
727
|
+
}
|
|
728
|
+
</div>
|
|
729
|
+
`,
|
|
730
|
+
imports: [DbxQuizResetButtonComponent],
|
|
731
|
+
standalone: true
|
|
732
|
+
}]
|
|
733
|
+
}], propDecorators: { input: [{ type: i0.Input, args: [{ isSignal: true, alias: "input", required: false }] }] } });
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Creates a Likert scale question config with agreement prompt (Strongly Disagree to Strongly Agree).
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```ts
|
|
740
|
+
* instance.config.set(quizAgreementPrompt('I feel confident leading under pressure.'));
|
|
741
|
+
* // { prompt: 'Please rate how much you agree...', text: '...', guidance: '1 = Strongly Disagree, 5 = Strongly Agree' }
|
|
742
|
+
* ```
|
|
743
|
+
*/
|
|
744
|
+
function quizAgreementPrompt(text) {
|
|
745
|
+
return {
|
|
746
|
+
prompt: 'Please rate how much you agree with the following statement:',
|
|
747
|
+
text,
|
|
748
|
+
guidance: '1 = Strongly Disagree, 5 = Strongly Agree'
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Creates a Likert scale question config with frequency prompt (Never to Always).
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```ts
|
|
756
|
+
* instance.config.set(quizFrequencyPrompt('I break vague direction into first steps.'));
|
|
757
|
+
* // { prompt: 'Please rate how much you agree...', text: '...', guidance: '1 = Never, 5 = Always' }
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
function quizFrequencyPrompt(text) {
|
|
761
|
+
return {
|
|
762
|
+
prompt: 'Please rate how much you agree with the following statement:',
|
|
763
|
+
text,
|
|
764
|
+
guidance: '1 = Never, 5 = Always'
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Generated bundle index. Do not edit.
|
|
770
|
+
*/
|
|
771
|
+
|
|
772
|
+
export { DbxQuizPostQuizComponent, DbxQuizResetButtonComponent, DbxQuizScoreComponent, QuizAnswerMultipleChoiceComponent, QuizAnswerNumberComponent, QuizComponent, QuizPreQuizIntroComponent, QuizQuestionAccessor, QuizQuestionTextComponent, QuizStore, provideCurrentQuestionQuizQuestionAccessor, quizAgreementPrompt, quizFrequencyPrompt };
|
|
773
|
+
//# sourceMappingURL=dereekb-dbx-form-quiz.mjs.map
|