@pie-element/mc-populated-blank 0.2.1 → 0.2.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-element/mc-populated-blank",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Multiple-choice option populates an inline blank in a sentence template (Svelte 5 PIE element)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -43,6 +43,7 @@
43
43
  "clean": "rm -rf dist tsconfig.tsbuildinfo"
44
44
  },
45
45
  "dependencies": {
46
+ "@pie-lib-svelte/styling": "workspace:*",
46
47
  "lodash-es": "^4.17.21"
47
48
  },
48
49
  "peerDependencies": {
@@ -91,14 +91,14 @@ const shouldLockChoices = (question: any, env: any) => {
91
91
  return env?.role === 'instructor';
92
92
  };
93
93
 
94
- const shuffleArray = <T>(items: T[]): T[] => {
94
+ function shuffleArray<T>(items: T[]): T[] {
95
95
  const out = [...items];
96
96
  for (let i = out.length - 1; i > 0; i--) {
97
97
  const j = Math.floor(Math.random() * (i + 1));
98
98
  [out[i], out[j]] = [out[j], out[i]];
99
99
  }
100
100
  return out;
101
- };
101
+ }
102
102
 
103
103
  const getStoredShuffle = (session: any): string[] =>
104
104
  Array.isArray(session?.data?.shuffledValues)
@@ -11,6 +11,7 @@
11
11
  />
12
12
 
13
13
  <script lang="ts">
14
+ import { color } from '@pie-lib-svelte/styling';
14
15
  /** Must match controller `BLANK_TOKEN` (kept local to avoid pulling controller into delivery). */
15
16
  const BLANK_TOKEN = '{{blank}}';
16
17
  const DEFAULT_LAYOUT_LIMITS = {
@@ -102,8 +103,12 @@ let toggleCorrectAnswerButtonEl = $state<HTMLButtonElement | null>(null);
102
103
  let choicesGroupEl = $state<HTMLDivElement | null>(null);
103
104
  const instanceId = `mc-populated-blank-${Math.random().toString(36).slice(2, 10)}`;
104
105
 
105
- const isEvaluateMode = $derived(model?.env?.mode === 'evaluate');
106
+ const deliveryMode = $derived((model?.mode || model?.env?.mode || '').toString());
107
+ const isEvaluateMode = $derived(deliveryMode === 'evaluate');
106
108
  const correctness = $derived(model?.correctness);
109
+ const isResponseCorrect = $derived(
110
+ typeof model?.responseCorrect === 'boolean' ? model.responseCorrect : correctness === 'correct'
111
+ );
107
112
  const isCorrect = $derived(correctness === 'correct');
108
113
  const isIncorrect = $derived(correctness === 'incorrect');
109
114
  const isAudioOnlyMode = $derived(model?.interactionMode === 'audio_mc_only');
@@ -156,6 +161,23 @@ const layoutLimits = $derived.by(() => {
156
161
  ...configured,
157
162
  };
158
163
  });
164
+ const correctAnswerStyleVars = $derived.by(() =>
165
+ [
166
+ `--pie-correct-answer-toggle-label-color:${color.text()}`,
167
+ `--pie-correct-answer-toggle-icon-open-bg:${color.tertiaryLight()}`,
168
+ `--pie-correct-answer-toggle-icon-closed-bg:${color.backgroundDark()}`,
169
+ `--pie-correct-answer-toggle-icon-glyph-color:${color.tertiary()}`,
170
+ `--pie-correct-answer-choice-hover-bg:${color.backgroundDark()}`,
171
+ `--pie-correct-answer-choice-selected-bg:${color.secondaryBackground()}`,
172
+ `--pie-correct-answer-choice-correct-bg:${color.correctSecondary()}`,
173
+ `--pie-correct-answer-choice-incorrect-bg:${color.incorrectSecondary()}`,
174
+ `--pie-correct-answer-choice-correct-border:${color.correctTertiary()}`,
175
+ `--pie-correct-answer-choice-incorrect-border:${color.incorrectWithIcon()}`,
176
+ `--pie-correct-answer-feedback-correct-bg:${color.correctWithIcon()}`,
177
+ `--pie-correct-answer-feedback-incorrect-bg:${color.incorrectWithIcon()}`,
178
+ `--pie-correct-answer-feedback-glyph-color:${color.white()}`,
179
+ ].join(';')
180
+ );
159
181
  const rootStyle = $derived.by(() =>
160
182
  [
161
183
  `--mpb-listen-button-size:${layoutLimits.listenButtonSizePx}px`,
@@ -192,6 +214,7 @@ const rootStyle = $derived.by(() =>
192
214
  `--mpb-inline-grid-row-gap:${layoutLimits.inlineGridRowGapRem}rem`,
193
215
  `--mpb-inline-template-margin-top:${layoutLimits.inlineTemplateMarginTopRem}rem`,
194
216
  `--mpb-inline-choices-margin-top:${layoutLimits.inlineChoicesMarginTopRem}rem`,
217
+ correctAnswerStyleVars,
195
218
  ].join(';')
196
219
  );
197
220
  const useFeatureButtonAudio = $derived.by(() => {
@@ -240,9 +263,19 @@ const selectedId = $derived(session?.choiceId || localChoiceId || '');
240
263
  const radioGroupName = $derived(`${instanceId}-choice-group-${model?.id || '1'}`);
241
264
 
242
265
  let showCorrectAnswer = $state(false);
266
+ const alwaysShowCorrect = $derived(
267
+ !!model?.alwaysShowCorrect || !!options?.alwaysShowCorrect || !!options?.addCorrectResponse
268
+ );
269
+ const canRevealCorrectAnswer = $derived(
270
+ !!model?.correctChoiceId && (isEvaluateMode || alwaysShowCorrect)
271
+ );
272
+ const shouldShowCorrectAnswer = $derived(alwaysShowCorrect || showCorrectAnswer);
273
+ const shouldShowCorrectAnswerToggle = $derived(
274
+ isEvaluateMode && !isResponseCorrect && !!model?.correctChoiceId && !alwaysShowCorrect
275
+ );
243
276
 
244
277
  const displayChoiceId = $derived.by(() => {
245
- if (isEvaluateMode && showCorrectAnswer && model?.correctChoiceId) {
278
+ if (canRevealCorrectAnswer && shouldShowCorrectAnswer && model?.correctChoiceId) {
246
279
  return model.correctChoiceId;
247
280
  }
248
281
  return selectedId;
@@ -295,11 +328,39 @@ const audioErrorMessage = $derived.by(() =>
295
328
  );
296
329
 
297
330
  const resultText = $derived.by(() => {
298
- if (!isEvaluateMode || showCorrectAnswer) return '';
331
+ if (!isEvaluateMode || shouldShowCorrectAnswer) return '';
299
332
  if (isCorrect) return 'Correct answer selected';
300
333
  if (isIncorrect && selectedId) return 'Incorrect answer selected';
301
334
  return '';
302
335
  });
336
+ const choiceCorrectnessById = $derived.by(() => {
337
+ const map = new Map<string, 'correct' | 'incorrect'>();
338
+ const correctChoiceId = String(model?.correctChoiceId || '');
339
+ const activeSelectedId = String(selectedId || '');
340
+
341
+ if (!isEvaluateMode || !correctChoiceId) {
342
+ return map;
343
+ }
344
+
345
+ if (shouldShowCorrectAnswer && canRevealCorrectAnswer) {
346
+ map.set(correctChoiceId, 'correct');
347
+ return map;
348
+ }
349
+
350
+ if (!activeSelectedId) {
351
+ map.set(correctChoiceId, 'incorrect');
352
+ return map;
353
+ }
354
+
355
+ if (activeSelectedId === correctChoiceId) {
356
+ map.set(correctChoiceId, 'correct');
357
+ return map;
358
+ }
359
+
360
+ map.set(activeSelectedId, 'incorrect');
361
+ map.set(correctChoiceId, 'incorrect');
362
+ return map;
363
+ });
303
364
 
304
365
  function emitSession(updatedSession: any, sourceEl?: HTMLElement | null) {
305
366
  const host = getHostElement(sourceEl);
@@ -331,9 +392,16 @@ function onRadioChange(e: Event) {
331
392
  }
332
393
 
333
394
  function toggleCorrectAnswer() {
395
+ if (!shouldShowCorrectAnswerToggle) return;
334
396
  showCorrectAnswer = !showCorrectAnswer;
335
397
  }
336
398
 
399
+ $effect(() => {
400
+ if (!isEvaluateMode || isResponseCorrect || alwaysShowCorrect) {
401
+ showCorrectAnswer = false;
402
+ }
403
+ });
404
+
337
405
  function onRadioGroupKeydown(e: KeyboardEvent) {
338
406
  if (model?.disabled) return;
339
407
  const key = e.key;
@@ -511,6 +579,74 @@ $effect(() => {
511
579
  <div class="mb-4 prose pie-prompt" id={promptId}>{@html model.prompt}</div>
512
580
  {/if}
513
581
 
582
+ {#if shouldShowCorrectAnswerToggle}
583
+ <div class="pie-correct-answer-toggle-row">
584
+ <button
585
+ bind:this={toggleCorrectAnswerButtonEl}
586
+ type="button"
587
+ class="mb-3 pie-toggle-correct-answer"
588
+ style="gap:var(--mpb-toggle-button-gap, 0.5rem);"
589
+ aria-pressed={shouldShowCorrectAnswer}
590
+ data-testid="show-correct-answer"
591
+ >
592
+ <span class="pie-correct-answer-toggle-content">
593
+ <span class="pie-correct-answer-toggle-icon-holder" aria-hidden="true">
594
+ {#if shouldShowCorrectAnswer}
595
+ <svg
596
+ class="pie-correct-answer-toggle-svg"
597
+ preserveAspectRatio="xMinYMin meet"
598
+ version="1.1"
599
+ viewBox="-283 359 34 35"
600
+ >
601
+ <circle cx="-266" cy="375.9" r="14" fill="var(--pie-correct-answer-toggle-icon-open-bg, #bce2ff)" />
602
+ <path
603
+ d="M-280.5,375.9c0-8,6.5-14.5,14.5-14.5s14.5,6.5,14.5,14.5s-6.5,14.5-14.5,14.5S-280.5,383.9-280.5,375.9z M-279.5,375.9c0,7.4,6.1,13.5,13.5,13.5c7.4,0,13.5-6.1,13.5-13.5s-6.1-13.5-13.5-13.5C-273.4,362.4-279.5,368.5-279.5,375.9z"
604
+ fill="var(--pie-correct-answer-toggle-icon-open-bg, #bce2ff)"
605
+ />
606
+ <polygon
607
+ points="-265.4,383.1 -258.6,377.2 -261.2,374.2 -264.3,376.9 -268.9,368.7 -272.4,370.6"
608
+ fill="var(--pie-correct-answer-toggle-icon-glyph-color, #1a9cff)"
609
+ />
610
+ </svg>
611
+ {:else}
612
+ <svg
613
+ class="pie-correct-answer-toggle-svg"
614
+ preserveAspectRatio="xMinYMin meet"
615
+ version="1.1"
616
+ viewBox="-129.5 127 34 35"
617
+ >
618
+ <path
619
+ d="M-112.9,160.4c-8.5,0-15.5-6.9-15.5-15.5c0-8.5,6.9-15.5,15.5-15.5s15.5,6.9,15.5,15.5 C-97.4,153.5-104.3,160.4-112.9,160.4z"
620
+ fill="#D0CAC5"
621
+ stroke="#E6E3E0"
622
+ stroke-width="0.75"
623
+ />
624
+ <path
625
+ d="M-113.2,159c-8,0-14.5-6.5-14.5-14.5s6.5-14.5,14.5-14.5s14.5,6.5,14.5,14.5S-105.2,159-113.2,159z"
626
+ fill="#B3ABA4"
627
+ stroke="#CDC7C2"
628
+ stroke-width="0.5"
629
+ />
630
+ <circle cx="-114.2" cy="143.5" r="14" fill="white" />
631
+ <path
632
+ d="M-114.2,158c-8,0-14.5-6.5-14.5-14.5s6.5-14.5,14.5-14.5s14.5,6.5,14.5,14.5S-106.2,158-114.2,158z M-114.2,130c-7.4,0-13.5,6.1-13.5,13.5s6.1,13.5,13.5,13.5s13.5-6.1,13.5-13.5S-106.8,130-114.2,130z"
633
+ fill="var(--pie-correct-answer-toggle-icon-closed-bg, #bce2ff)"
634
+ />
635
+ <polygon
636
+ points="-114.8,150.7 -121.6,144.8 -119,141.8 -115.9,144.5 -111.3,136.3 -107.8,138.2"
637
+ fill="var(--pie-correct-answer-toggle-icon-glyph-color, #1a9cff)"
638
+ />
639
+ </svg>
640
+ {/if}
641
+ </span>
642
+ <span class="pie-correct-answer-toggle-label">
643
+ {shouldShowCorrectAnswer ? uiText.hideCorrectAnswer : uiText.showCorrectAnswer}
644
+ </span>
645
+ </span>
646
+ </button>
647
+ </div>
648
+ {/if}
649
+
514
650
  {#if model?.hasAudio}
515
651
  <div class="mb-4 audio-container pie-audio-container">
516
652
  {#if hasPlayableAudio && useFeatureButtonAudio}
@@ -609,19 +745,6 @@ $effect(() => {
609
745
  </div>
610
746
  {/if}
611
747
 
612
- {#if isEvaluateMode && isIncorrect}
613
- <button
614
- bind:this={toggleCorrectAnswerButtonEl}
615
- type="button"
616
- class="mb-3 flex items-center cursor-pointer select-none pie-toggle-correct-answer"
617
- style="gap:var(--mpb-toggle-button-gap, 0.5rem);"
618
- aria-pressed={showCorrectAnswer}
619
- >
620
- <span class="text-sm hover:underline">
621
- {showCorrectAnswer ? uiText.hideCorrectAnswer : uiText.showCorrectAnswer}
622
- </span>
623
- </button>
624
- {/if}
625
748
  {#if resultText}
626
749
  <p id={resultId} class="sr-only pie-result-feedback" role="status" aria-live="polite">{resultText}</p>
627
750
  {/if}
@@ -639,8 +762,9 @@ $effect(() => {
639
762
  aria-describedby={resultText ? resultId : undefined}
640
763
  >
641
764
  {#each choices as c (c.id)}
765
+ {@const choiceCorrectness = choiceCorrectnessById.get(c.id)}
642
766
  <div
643
- class={`flex items-start choice-row pie-choice ${isHorizontalChoices ? 'choice-row-horizontal pie-choice-horizontal' : ''} ${((showCorrectAnswer && isEvaluateMode ? model?.correctChoiceId : selectedId) === c.id) ? 'is-selected pie-choice-selected' : ''}`}
767
+ class={`flex items-start choice-row pie-choice ${isHorizontalChoices ? 'choice-row-horizontal pie-choice-horizontal' : ''} ${((shouldShowCorrectAnswer && canRevealCorrectAnswer ? model?.correctChoiceId : selectedId) === c.id) ? 'is-selected pie-choice-selected' : ''} ${choiceCorrectness ? `choice-${choiceCorrectness} pie-choice-${choiceCorrectness}` : ''}`}
644
768
  style="gap:var(--mpb-choice-row-gap, 0.5rem);"
645
769
  >
646
770
  {#if isHorizontalChoices}
@@ -666,7 +790,7 @@ $effect(() => {
666
790
  id={`${instanceId}-opt-${c.id}`}
667
791
  value={c.id}
668
792
  checked={
669
- (showCorrectAnswer && isEvaluateMode ? model?.correctChoiceId : selectedId) === c.id
793
+ (shouldShowCorrectAnswer && canRevealCorrectAnswer ? model?.correctChoiceId : selectedId) === c.id
670
794
  }
671
795
  disabled={model?.disabled}
672
796
  class="choice-radio-bottom pie-choice-radio pie-choice-radio-bottom"
@@ -679,7 +803,7 @@ $effect(() => {
679
803
  id={`${instanceId}-opt-${c.id}`}
680
804
  value={c.id}
681
805
  checked={
682
- (showCorrectAnswer && isEvaluateMode ? model?.correctChoiceId : selectedId) === c.id
806
+ (shouldShowCorrectAnswer && canRevealCorrectAnswer ? model?.correctChoiceId : selectedId) === c.id
683
807
  }
684
808
  disabled={model?.disabled}
685
809
  class="choice-radio-inline pie-choice-radio pie-choice-radio-inline"
@@ -697,16 +821,13 @@ $effect(() => {
697
821
  {/if}
698
822
  </label>
699
823
  {/if}
700
- {#if isEvaluateMode && !showCorrectAnswer}
701
- {#if selectedId === c.id && isCorrect}
702
- <span class="text-green-600 text-sm font-medium pie-choice-feedback-correct" aria-hidden="true">
703
- ✓
704
- </span>
705
- {:else if selectedId === c.id && isIncorrect}
706
- <span class="text-red-600 text-sm font-medium pie-choice-feedback-incorrect" aria-hidden="true">
707
- ✗
708
- </span>
709
- {/if}
824
+ {#if isEvaluateMode && choiceCorrectness}
825
+ <span
826
+ class={`pie-choice-feedback-badge ${choiceCorrectness === 'correct' ? 'pie-choice-feedback-correct' : 'pie-choice-feedback-incorrect'}`}
827
+ aria-hidden="true"
828
+ >
829
+ {choiceCorrectness === 'correct' ? '✓' : '✕'}
830
+ </span>
710
831
  {/if}
711
832
  </div>
712
833
  {/each}
@@ -744,6 +865,57 @@ $effect(() => {
744
865
  vertical-align: baseline;
745
866
  }
746
867
 
868
+ .pie-toggle-correct-answer {
869
+ width: 100%;
870
+ cursor: pointer;
871
+ border: 0;
872
+ background: transparent;
873
+ padding: 0;
874
+ display: flex;
875
+ justify-content: center;
876
+ text-align: center;
877
+ color: var(--pie-correct-answer-toggle-label-color, var(--pie-text, black));
878
+ }
879
+
880
+ .pie-correct-answer-toggle-row {
881
+ width: 100%;
882
+ display: flex;
883
+ justify-content: flex-end;
884
+ }
885
+
886
+ .pie-correct-answer-toggle-content {
887
+ display: flex;
888
+ margin: 0 auto;
889
+ align-items: center;
890
+ }
891
+
892
+ .pie-correct-answer-toggle-icon-holder {
893
+ width: 25px;
894
+ margin-right: 5px;
895
+ display: inline-flex;
896
+ align-items: center;
897
+ justify-content: center;
898
+ }
899
+
900
+ .pie-correct-answer-toggle-svg {
901
+ width: 25px;
902
+ height: 25px;
903
+ }
904
+
905
+ .pie-correct-answer-toggle-label {
906
+ width: fit-content;
907
+ min-width: 140px;
908
+ align-self: center;
909
+ vertical-align: middle;
910
+ font-weight: 400;
911
+ user-select: none;
912
+ }
913
+
914
+ .pie-toggle-correct-answer:hover .pie-correct-answer-toggle-label,
915
+ .pie-toggle-correct-answer:focus-visible .pie-correct-answer-toggle-label {
916
+ text-decoration: underline;
917
+ }
918
+
747
919
  .template-line {
748
920
  white-space: pre-wrap;
749
921
  }
@@ -815,15 +987,53 @@ $effect(() => {
815
987
  }
816
988
 
817
989
  .choice-row-horizontal:hover .choice-tile {
818
- background: #ececec;
990
+ background: var(--pie-correct-answer-choice-hover-bg, #ececec);
819
991
  }
820
992
 
821
993
  .choice-row-horizontal.is-selected .choice-tile {
822
- background: #eceabf;
994
+ background: var(--pie-correct-answer-choice-selected-bg, #f1f1f1);
823
995
  }
824
996
 
825
997
  .choice-row-horizontal.is-selected:hover .choice-tile {
826
- background: #eceabf;
998
+ background: var(--pie-correct-answer-choice-selected-bg, #f1f1f1);
999
+ }
1000
+
1001
+ .pie-choice.choice-correct {
1002
+ border-left: 3px solid var(--pie-correct-answer-choice-correct-border, #0ea449);
1003
+ }
1004
+
1005
+ .pie-choice.choice-incorrect {
1006
+ border-left: 3px solid var(--pie-correct-answer-choice-incorrect-border, #bf0d00);
1007
+ }
1008
+
1009
+ .pie-choice-horizontal.choice-correct .choice-tile {
1010
+ background: var(--pie-correct-answer-choice-correct-bg, #e8f5e9);
1011
+ }
1012
+
1013
+ .pie-choice-horizontal.choice-incorrect .choice-tile {
1014
+ background: var(--pie-correct-answer-choice-incorrect-bg, #ffebee);
1015
+ }
1016
+
1017
+ .pie-choice-feedback-badge {
1018
+ margin-left: auto;
1019
+ display: inline-flex;
1020
+ align-items: center;
1021
+ justify-content: center;
1022
+ width: 1.1rem;
1023
+ height: 1.1rem;
1024
+ border-radius: 9999px;
1025
+ font-size: 0.72rem;
1026
+ line-height: 1;
1027
+ font-weight: 700;
1028
+ color: var(--pie-correct-answer-feedback-glyph-color, #fff);
1029
+ }
1030
+
1031
+ .pie-choice-feedback-correct {
1032
+ background: var(--pie-correct-answer-feedback-correct-bg, #087d38);
1033
+ }
1034
+
1035
+ .pie-choice-feedback-incorrect {
1036
+ background: var(--pie-correct-answer-feedback-incorrect-bg, #bf0d00);
827
1037
  }
828
1038
 
829
1039
  .choice-row-horizontal :global(p) {