@pie-element/mc-populated-blank 0.2.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-element/mc-populated-blank",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",
@@ -22,6 +22,8 @@ export const DEFAULT_LAYOUT_LIMITS = {
22
22
  horizontalChoiceRadioTopMarginRem: 0.5,
23
23
  audioBlankTemplateMarginTopRem: 0.8,
24
24
  audioBlankTemplateMarginBottomRem: 1.8,
25
+ audioInstructionsMaxWidthPx: 875,
26
+ narrowHorizontalChoiceMaxWidthPx: 230,
25
27
  stimulusGridColumnGapRem: 2,
26
28
  stimulusGridRowGapRem: 0.7,
27
29
  stimulusSentenceMarginTopRem: 0.2,
@@ -163,10 +163,7 @@ export const model = async (question: any, session: any, env: any, updateSession
163
163
  typeof normalizedQuestion.layoutProfilePresets === 'object'
164
164
  ? normalizedQuestion.layoutProfilePresets
165
165
  : {},
166
- layoutLimits: {
167
- ...DEFAULT_LAYOUT_LIMITS,
168
- ...(normalizedQuestion.layoutLimits || {}),
169
- },
166
+ layoutLimits: normalizedQuestion.layoutLimits || { ...DEFAULT_LAYOUT_LIMITS },
170
167
  audioButtonSkin:
171
168
  normalizedQuestion.audioButtonSkin && typeof normalizedQuestion.audioButtonSkin === 'object'
172
169
  ? normalizedQuestion.audioButtonSkin
@@ -324,6 +321,8 @@ export const validate = (model: any = {}, _config: any = {}) => {
324
321
  'horizontalChoiceRadioTopMarginRem',
325
322
  'audioBlankTemplateMarginTopRem',
326
323
  'audioBlankTemplateMarginBottomRem',
324
+ 'audioInstructionsMaxWidthPx',
325
+ 'narrowHorizontalChoiceMaxWidthPx',
327
326
  'stimulusGridColumnGapRem',
328
327
  'stimulusGridRowGapRem',
329
328
  'stimulusSentenceMarginTopRem',
@@ -1,6 +1,6 @@
1
1
  <svelte:options
2
2
  customElement={{
3
- tag: 'mc-populated-blank-element',
3
+ tag: 'mc-populated-blank',
4
4
  shadow: 'none',
5
5
  props: {
6
6
  model: { type: 'Object' },
@@ -34,6 +34,8 @@ const DEFAULT_LAYOUT_LIMITS = {
34
34
  horizontalChoiceRadioTopMarginRem: 0.5,
35
35
  audioBlankTemplateMarginTopRem: 0.8,
36
36
  audioBlankTemplateMarginBottomRem: 1.8,
37
+ audioInstructionsMaxWidthPx: 875,
38
+ narrowHorizontalChoiceMaxWidthPx: 230,
37
39
  stimulusGridColumnGapRem: 2,
38
40
  stimulusGridRowGapRem: 0.7,
39
41
  stimulusSentenceMarginTopRem: 0.2,
@@ -88,21 +90,25 @@ const DEFAULT_UI_TEXT = {
88
90
  listenLabelEs: 'Escuchar',
89
91
  } as const;
90
92
 
91
- let props = $props<{ model?: any; session?: any; options?: any }>();
93
+ let { model, session, options } = $props<{ model?: any; session?: any; options?: any }>();
92
94
  let audioEl = $state<HTMLAudioElement | null>(null);
93
95
  let isMediaPlaying = $state(false);
94
96
  let localChoiceId = $state('');
95
97
  let autoPlayPromptOpen = $state(false);
96
98
  let autoplayAttempted = $state(false);
99
+ let featureAudioButtonEl = $state<HTMLButtonElement | null>(null);
100
+ let autoplayEnableButtonEl = $state<HTMLButtonElement | null>(null);
101
+ let toggleCorrectAnswerButtonEl = $state<HTMLButtonElement | null>(null);
102
+ let choicesGroupEl = $state<HTMLDivElement | null>(null);
97
103
  const instanceId = `mc-populated-blank-${Math.random().toString(36).slice(2, 10)}`;
98
104
 
99
- const isEvaluateMode = $derived(props?.model?.env?.mode === 'evaluate');
100
- const correctness = $derived(props?.model?.correctness);
105
+ const isEvaluateMode = $derived(model?.env?.mode === 'evaluate');
106
+ const correctness = $derived(model?.correctness);
101
107
  const isCorrect = $derived(correctness === 'correct');
102
108
  const isIncorrect = $derived(correctness === 'incorrect');
103
- const isAudioOnlyMode = $derived(props?.model?.interactionMode === 'audio_mc_only');
109
+ const isAudioOnlyMode = $derived(model?.interactionMode === 'audio_mc_only');
104
110
  const isBlankOnlyTemplate = $derived.by(() => {
105
- const t = props?.model?.template || '';
111
+ const t = model?.template || '';
106
112
  if (!t) return false;
107
113
  const plain = t
108
114
  .replace(/<[^>]+>/g, '')
@@ -110,27 +116,27 @@ const isBlankOnlyTemplate = $derived.by(() => {
110
116
  .trim();
111
117
  return plain === BLANK_TOKEN;
112
118
  });
113
- const layoutProfile = $derived(props?.model?.layoutProfile || '');
119
+ const layoutProfile = $derived(model?.layoutProfile || '');
114
120
  const choiceLayout = $derived(
115
- props?.model?.choiceLayout || (isAudioOnlyMode || isBlankOnlyTemplate ? 'horizontal' : 'vertical')
121
+ model?.choiceLayout || (isAudioOnlyMode || isBlankOnlyTemplate ? 'horizontal' : 'vertical')
116
122
  );
117
123
  const isHorizontalChoices = $derived(choiceLayout === 'horizontal');
118
- const showVisibleTranscript = $derived(!!props?.model?.showVisibleTranscript);
119
- const hasPlayableAudio = $derived(!!props?.model?.hasAudio && !!props?.model?.audioUrl);
120
- const hasAudioButMissingResource = $derived(!!props?.model?.hasAudio && !props?.model?.audioUrl);
124
+ const showVisibleTranscript = $derived(!!model?.showVisibleTranscript);
125
+ const hasPlayableAudio = $derived(!!model?.hasAudio && !!model?.audioUrl);
126
+ const hasAudioButMissingResource = $derived(!!model?.hasAudio && !model?.audioUrl);
121
127
  const hasInlineSentenceAudioLayout = $derived(
122
- layoutProfile === 'inline_sentence' && !!props?.model?.hasAudio
128
+ layoutProfile === 'inline_sentence' && !!model?.hasAudio
123
129
  );
124
130
  const uiText = $derived.by(() => ({
125
131
  ...DEFAULT_UI_TEXT,
126
- ...(props?.model?.uiText || {}),
132
+ ...(model?.uiText || {}),
127
133
  }));
128
134
  const profilePresetLimits = $derived.by(() => {
129
135
  const profile = String(layoutProfile || '');
130
136
  const defaults = DEFAULT_LAYOUT_PROFILE_PRESETS[profile] || {};
131
137
  const customPresets =
132
- props?.model?.layoutProfilePresets && typeof props.model.layoutProfilePresets === 'object'
133
- ? props.model.layoutProfilePresets
138
+ model?.layoutProfilePresets && typeof model.layoutProfilePresets === 'object'
139
+ ? model.layoutProfilePresets
134
140
  : {};
135
141
  const custom =
136
142
  customPresets[profile] && typeof customPresets[profile] === 'object'
@@ -143,9 +149,7 @@ const profilePresetLimits = $derived.by(() => {
143
149
  });
144
150
  const layoutLimits = $derived.by(() => {
145
151
  const configured =
146
- props?.model?.layoutLimits && typeof props.model.layoutLimits === 'object'
147
- ? props.model.layoutLimits
148
- : {};
152
+ model?.layoutLimits && typeof model.layoutLimits === 'object' ? model.layoutLimits : {};
149
153
  return {
150
154
  ...DEFAULT_LAYOUT_LIMITS,
151
155
  ...profilePresetLimits,
@@ -173,6 +177,8 @@ const rootStyle = $derived.by(() =>
173
177
  `--mpb-horizontal-choice-radio-top-margin:${layoutLimits.horizontalChoiceRadioTopMarginRem}rem`,
174
178
  `--mpb-audio-blank-template-margin-top:${layoutLimits.audioBlankTemplateMarginTopRem}rem`,
175
179
  `--mpb-audio-blank-template-margin-bottom:${layoutLimits.audioBlankTemplateMarginBottomRem}rem`,
180
+ `--mpb-audio-instructions-max-width:${layoutLimits.audioInstructionsMaxWidthPx}px`,
181
+ `--mpb-narrow-choice-max-width:${layoutLimits.narrowHorizontalChoiceMaxWidthPx}px`,
176
182
  `--mpb-stimulus-grid-column-gap:${layoutLimits.stimulusGridColumnGapRem}rem`,
177
183
  `--mpb-stimulus-grid-row-gap:${layoutLimits.stimulusGridRowGapRem}rem`,
178
184
  `--mpb-stimulus-sentence-margin-top:${layoutLimits.stimulusSentenceMarginTopRem}rem`,
@@ -189,12 +195,12 @@ const rootStyle = $derived.by(() =>
189
195
  ].join(';')
190
196
  );
191
197
  const useFeatureButtonAudio = $derived.by(() => {
192
- if (typeof props?.model?.useFeatureButtonAudio === 'boolean') {
193
- return !!props.model.useFeatureButtonAudio;
198
+ if (typeof model?.useFeatureButtonAudio === 'boolean') {
199
+ return !!model.useFeatureButtonAudio;
194
200
  }
195
201
  const profile = layoutProfile || '';
196
202
  return (
197
- !!props?.model?.hasAudio &&
203
+ !!model?.hasAudio &&
198
204
  (profile === 'audio_blank_only' ||
199
205
  profile === 'stimulus_image_blank' ||
200
206
  profile === 'token_sequence')
@@ -202,7 +208,7 @@ const useFeatureButtonAudio = $derived.by(() => {
202
208
  });
203
209
 
204
210
  const templateParts = $derived.by(() => {
205
- const t = props?.model?.template || '';
211
+ const t = model?.template || '';
206
212
  const idx = t.indexOf(BLANK_TOKEN);
207
213
  if (idx < 0) {
208
214
  return { before: t, after: '' };
@@ -228,16 +234,16 @@ const blankBorderWidth = $derived.by(() => {
228
234
  return `${layoutLimits.blankUnderlineWidthPx}px`;
229
235
  });
230
236
 
231
- const choices = $derived(Array.isArray(props?.model?.choices) ? props.model.choices : []);
232
- const choiceMode = $derived(props?.model?.choiceMode || 'text');
233
- const selectedId = $derived(props?.session?.choiceId || localChoiceId || '');
234
- const radioGroupName = $derived(`${instanceId}-choice-group-${props?.model?.id || '1'}`);
237
+ const choices = $derived(Array.isArray(model?.choices) ? model.choices : []);
238
+ const choiceMode = $derived(model?.choiceMode || 'text');
239
+ const selectedId = $derived(session?.choiceId || localChoiceId || '');
240
+ const radioGroupName = $derived(`${instanceId}-choice-group-${model?.id || '1'}`);
235
241
 
236
242
  let showCorrectAnswer = $state(false);
237
243
 
238
244
  const displayChoiceId = $derived.by(() => {
239
- if (isEvaluateMode && showCorrectAnswer && props?.model?.correctChoiceId) {
240
- return props.model.correctChoiceId;
245
+ if (isEvaluateMode && showCorrectAnswer && model?.correctChoiceId) {
246
+ return model.correctChoiceId;
241
247
  }
242
248
  return selectedId;
243
249
  });
@@ -245,7 +251,7 @@ const displayChoiceId = $derived.by(() => {
245
251
  const displayChoice = $derived.by(() => choices.find((c: any) => c.id === displayChoiceId));
246
252
 
247
253
  const legendText = $derived.by(() => {
248
- const plain = (props?.model?.prompt || '')
254
+ const plain = (model?.prompt || '')
249
255
  .replace(/<[^>]+>/g, ' ')
250
256
  .replace(/\s+/g, ' ')
251
257
  .trim();
@@ -263,19 +269,25 @@ const transcriptId = $derived(`${instanceId}-transcript`);
263
269
  const legendId = $derived(`${instanceId}-choices-legend`);
264
270
  const resultId = $derived(`${instanceId}-result`);
265
271
 
266
- const choicesGroupLabelledBy = $derived(props?.model?.prompt ? promptId : legendId);
272
+ const choicesGroupLabelledBy = $derived(model?.prompt ? promptId : undefined);
273
+ const choicesGroupAriaLabel = $derived.by(() => {
274
+ if (model?.prompt) return undefined;
275
+ const explicit = String(model?.choiceGroupLabel || '').trim();
276
+ if (explicit) return explicit;
277
+ return legendText;
278
+ });
267
279
 
268
280
  const templateDescribedBy = $derived.by(() => {
269
281
  const ids: string[] = [];
270
- if (props?.model?.prompt) ids.push(promptId);
271
- if (props?.model?.hasAudio && props?.model?.audioTranscript) {
282
+ if (model?.prompt) ids.push(promptId);
283
+ if (model?.hasAudio && model?.audioTranscript) {
272
284
  ids.push(transcriptId);
273
285
  }
274
286
  return ids.length ? ids.join(' ') : undefined;
275
287
  });
276
288
 
277
289
  const lang = $derived.by(() => {
278
- const locale = props?.model?.locale || '';
290
+ const locale = model?.locale || '';
279
291
  return locale ? locale.slice(0, 2) : 'en';
280
292
  });
281
293
  const audioErrorMessage = $derived.by(() =>
@@ -290,9 +302,7 @@ const resultText = $derived.by(() => {
290
302
  });
291
303
 
292
304
  function emitSession(updatedSession: any, sourceEl?: HTMLElement | null) {
293
- const host =
294
- (sourceEl?.closest?.('mc-populated-blank-element') as any) ||
295
- (document.querySelector('mc-populated-blank-element') as any);
305
+ const host = getHostElement(sourceEl);
296
306
  if (host?.onSessionChange) {
297
307
  host.onSessionChange(updatedSession);
298
308
  return;
@@ -312,8 +322,8 @@ function onRadioChange(e: Event) {
312
322
  const choiceId = input.value;
313
323
  localChoiceId = choiceId;
314
324
  const updatedSession = {
315
- ...props.session,
316
- id: props.session?.id || props.model?.id || '1',
325
+ ...session,
326
+ id: session?.id || model?.id || '1',
317
327
  element: 'mc-populated-blank',
318
328
  choiceId,
319
329
  };
@@ -325,7 +335,7 @@ function toggleCorrectAnswer() {
325
335
  }
326
336
 
327
337
  function onRadioGroupKeydown(e: KeyboardEvent) {
328
- if (props?.model?.disabled) return;
338
+ if (model?.disabled) return;
329
339
  const key = e.key;
330
340
  if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(key)) return;
331
341
  e.preventDefault();
@@ -355,33 +365,48 @@ function handleEnableAutoplayClick() {
355
365
  }
356
366
 
357
367
  function onAudioPlaying(e: Event) {
358
- const host = (e.currentTarget as HTMLElement)?.closest('mc-populated-blank-element') as any;
368
+ const host = getHostElement(e.currentTarget as HTMLElement);
359
369
  isMediaPlaying = true;
360
370
  host?.onAudioStarted?.();
361
371
  autoPlayPromptOpen = false;
362
372
  }
363
373
 
364
374
  function onAudioEnded(e: Event) {
365
- const host = (e.currentTarget as HTMLElement)?.closest('mc-populated-blank-element') as any;
375
+ const host = getHostElement(e.currentTarget as HTMLElement);
366
376
  isMediaPlaying = false;
367
377
  host?.onAudioEnded?.();
368
378
  }
369
379
 
380
+ function getHostElement(sourceEl?: HTMLElement | null) {
381
+ let cursor: HTMLElement | null | undefined = sourceEl;
382
+ while (cursor) {
383
+ const maybeHost = cursor as any;
384
+ if (
385
+ typeof maybeHost?.onSessionChange === 'function' ||
386
+ typeof maybeHost?.onAudioStarted === 'function' ||
387
+ typeof maybeHost?.onAudioEnded === 'function'
388
+ ) {
389
+ return maybeHost;
390
+ }
391
+ cursor = cursor.parentElement;
392
+ }
393
+ return (document.querySelector('mc-populated-blank') as any) || null;
394
+ }
395
+
370
396
  function speechButtonLabel(locale = '') {
371
397
  return locale.toLowerCase().startsWith('es') ? uiText.listenLabelEs : uiText.listenLabelEn;
372
398
  }
373
399
 
374
400
  const featureAudioSkin = $derived.by(() => {
375
- const locale = String(props?.model?.locale || '').toLowerCase();
401
+ const locale = String(model?.locale || '').toLowerCase();
376
402
  const lang = locale.slice(0, 2);
377
403
  const byLocale =
378
- props?.model?.audioButtonSkinsByLocale &&
379
- typeof props.model.audioButtonSkinsByLocale === 'object'
380
- ? props.model.audioButtonSkinsByLocale
404
+ model?.audioButtonSkinsByLocale && typeof model.audioButtonSkinsByLocale === 'object'
405
+ ? model.audioButtonSkinsByLocale
381
406
  : {};
382
407
  const customSingle =
383
- props?.model?.audioButtonSkin && typeof props.model.audioButtonSkin === 'object'
384
- ? props.model.audioButtonSkin
408
+ model?.audioButtonSkin && typeof model.audioButtonSkin === 'object'
409
+ ? model.audioButtonSkin
385
410
  : null;
386
411
  const defaultSkin = locale.startsWith('es')
387
412
  ? DEFAULT_AUDIO_BUTTON_SKINS.es
@@ -398,17 +423,77 @@ function playFeatureAudio() {
398
423
  }
399
424
 
400
425
  $effect(() => {
401
- if (props?.session?.choiceId) {
402
- localChoiceId = props.session.choiceId;
426
+ if (session?.choiceId) {
427
+ localChoiceId = session.choiceId;
403
428
  }
404
429
  });
405
430
 
406
431
  $effect(() => {
407
- if (!props?.model?.hasAudio || !props?.model?.autoplayAudioEnabled) return;
432
+ const el = audioEl;
433
+ if (!el) return;
434
+ const handlePlaying = (e: Event) => onAudioPlaying(e);
435
+ const handleEnded = (e: Event) => onAudioEnded(e);
436
+ el.addEventListener('playing', handlePlaying);
437
+ el.addEventListener('ended', handleEnded);
438
+ return () => {
439
+ el.removeEventListener('playing', handlePlaying);
440
+ el.removeEventListener('ended', handleEnded);
441
+ };
442
+ });
443
+
444
+ $effect(() => {
445
+ const btn = featureAudioButtonEl;
446
+ if (!btn) return;
447
+ const handleClick = () => playFeatureAudio();
448
+ btn.addEventListener('click', handleClick);
449
+ return () => {
450
+ btn.removeEventListener('click', handleClick);
451
+ };
452
+ });
453
+
454
+ $effect(() => {
455
+ const btn = autoplayEnableButtonEl;
456
+ if (!btn) return;
457
+ const handleClick = () => handleEnableAutoplayClick();
458
+ btn.addEventListener('click', handleClick);
459
+ return () => {
460
+ btn.removeEventListener('click', handleClick);
461
+ };
462
+ });
463
+
464
+ $effect(() => {
465
+ const btn = toggleCorrectAnswerButtonEl;
466
+ if (!btn) return;
467
+ const handleClick = () => toggleCorrectAnswer();
468
+ btn.addEventListener('click', handleClick);
469
+ return () => {
470
+ btn.removeEventListener('click', handleClick);
471
+ };
472
+ });
473
+
474
+ $effect(() => {
475
+ const group = choicesGroupEl;
476
+ if (!group) return;
477
+ const handleChange = (e: Event) => {
478
+ const target = e.target as HTMLInputElement | null;
479
+ if (!target || target.type !== 'radio') return;
480
+ onRadioChange(e);
481
+ };
482
+ const handleKeydown = (e: KeyboardEvent) => onRadioGroupKeydown(e);
483
+ group.addEventListener('change', handleChange);
484
+ group.addEventListener('keydown', handleKeydown);
485
+ return () => {
486
+ group.removeEventListener('change', handleChange);
487
+ group.removeEventListener('keydown', handleKeydown);
488
+ };
489
+ });
490
+
491
+ $effect(() => {
492
+ if (!model?.hasAudio || !model?.autoplayAudioEnabled) return;
408
493
  if (autoplayAttempted) return;
409
494
 
410
495
  autoplayAttempted = true;
411
- if (audioEl && props?.model?.audioUrl) {
496
+ if (audioEl && model?.audioUrl) {
412
497
  // Browser autoplay restrictions vary. Try immediate play; if blocked show click-to-enable affordance.
413
498
  audioEl.play().catch(() => {
414
499
  autoPlayPromptOpen = true;
@@ -422,28 +507,26 @@ $effect(() => {
422
507
  lang={lang}
423
508
  style={rootStyle}
424
509
  >
425
- {#if props?.model?.prompt}
426
- <div class="mb-4 prose pie-prompt" id={promptId}>{@html props.model.prompt}</div>
510
+ {#if model?.prompt}
511
+ <div class="mb-4 prose pie-prompt" id={promptId}>{@html model.prompt}</div>
427
512
  {/if}
428
513
 
429
- {#if props?.model?.hasAudio}
514
+ {#if model?.hasAudio}
430
515
  <div class="mb-4 audio-container pie-audio-container">
431
516
  {#if hasPlayableAudio && useFeatureButtonAudio}
432
517
  <audio
433
518
  bind:this={audioEl}
434
519
  class="sr-only pie-audio-player"
435
520
  preload="metadata"
436
- src={props.model.audioUrl}
521
+ src={model.audioUrl}
437
522
  aria-hidden="true"
438
523
  tabindex="-1"
439
- on:playing={onAudioPlaying}
440
- on:ended={onAudioEnded}
441
524
  ></audio>
442
525
  <button
526
+ bind:this={featureAudioButtonEl}
443
527
  class="listen-button pie-listen-button rli-feature-audio"
444
528
  type="button"
445
- aria-label={speechButtonLabel(props?.model?.locale)}
446
- on:click={playFeatureAudio}
529
+ aria-label={speechButtonLabel(model?.locale)}
447
530
  >
448
531
  <img
449
532
  class={`listen-feature-icon pie-listen-icon rli-feature-listen ${isMediaPlaying ? '' : 'listen-active'}`}
@@ -464,18 +547,16 @@ $effect(() => {
464
547
  controls
465
548
  class="w-full max-w-md pie-audio-player"
466
549
  preload="metadata"
467
- src={props.model.audioUrl}
468
- aria-describedby={props?.model?.audioTranscript ? transcriptId : undefined}
469
- on:playing={onAudioPlaying}
470
- on:ended={onAudioEnded}
550
+ src={model.audioUrl}
551
+ aria-describedby={model?.audioTranscript ? transcriptId : undefined}
471
552
  >
472
553
  <track kind="captions" />
473
554
  </audio>
474
555
  {#if autoPlayPromptOpen}
475
556
  <button
557
+ bind:this={autoplayEnableButtonEl}
476
558
  class="mt-2 text-sm underline pie-audio-autoplay-enable"
477
559
  type="button"
478
- on:click={handleEnableAutoplayClick}
479
560
  >
480
561
  {uiText.clickToEnableAutoplay}
481
562
  </button>
@@ -483,20 +564,20 @@ $effect(() => {
483
564
  {:else if hasAudioButMissingResource}
484
565
  <p class="text-sm text-red-700 pie-audio-error" role="alert">{audioErrorMessage}</p>
485
566
  {/if}
486
- {#if props?.model?.audioTranscript}
567
+ {#if model?.audioTranscript}
487
568
  <p
488
569
  class={`text-sm mt-2 text-gray-700 pie-audio-transcript ${showVisibleTranscript ? '' : 'sr-only'}`}
489
570
  id={transcriptId}
490
571
  >
491
- <strong>{uiText.transcriptLabel}:</strong> {props.model.audioTranscript}
572
+ <strong>{uiText.transcriptLabel}:</strong> {model.audioTranscript}
492
573
  </p>
493
574
  {/if}
494
575
  </div>
495
576
  {/if}
496
577
 
497
- {#if props?.model?.sentenceHtml}
578
+ {#if model?.sentenceHtml}
498
579
  <div class="mb-3 prose prose-p:my-1 sentence-line pie-sentence-line" aria-describedby={templateDescribedBy}>
499
- {@html props.model.sentenceHtml}
580
+ {@html model.sentenceHtml}
500
581
  </div>
501
582
  {/if}
502
583
 
@@ -530,10 +611,10 @@ $effect(() => {
530
611
 
531
612
  {#if isEvaluateMode && isIncorrect}
532
613
  <button
614
+ bind:this={toggleCorrectAnswerButtonEl}
533
615
  type="button"
534
616
  class="mb-3 flex items-center cursor-pointer select-none pie-toggle-correct-answer"
535
617
  style="gap:var(--mpb-toggle-button-gap, 0.5rem);"
536
- on:click={toggleCorrectAnswer}
537
618
  aria-pressed={showCorrectAnswer}
538
619
  >
539
620
  <span class="text-sm hover:underline">
@@ -545,20 +626,21 @@ $effect(() => {
545
626
  <p id={resultId} class="sr-only pie-result-feedback" role="status" aria-live="polite">{resultText}</p>
546
627
  {/if}
547
628
 
548
- <fieldset class="border-0 p-0 m-0 pie-choices-fieldset" disabled={props?.model?.disabled}>
629
+ <fieldset class="border-0 p-0 m-0 pie-choices-fieldset" disabled={model?.disabled}>
549
630
  <legend class="sr-only pie-choices-legend" id={legendId}>{legendText}</legend>
550
631
  <div
632
+ bind:this={choicesGroupEl}
551
633
  class={`pie-choices ${isHorizontalChoices ? 'flex flex-row flex-wrap items-start justify-center' : 'flex flex-col'}`}
552
634
  style="gap:var(--mpb-choice-group-gap, 0.5rem);"
553
635
  role="radiogroup"
554
636
  tabindex="-1"
555
637
  aria-labelledby={choicesGroupLabelledBy}
638
+ aria-label={choicesGroupAriaLabel}
556
639
  aria-describedby={resultText ? resultId : undefined}
557
- on:keydown={onRadioGroupKeydown}
558
640
  >
559
641
  {#each choices as c (c.id)}
560
642
  <div
561
- class={`flex items-start choice-row pie-choice ${isHorizontalChoices ? 'choice-row-horizontal pie-choice-horizontal' : ''} ${((showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id) ? 'is-selected pie-choice-selected' : ''}`}
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' : ''}`}
562
644
  style="gap:var(--mpb-choice-row-gap, 0.5rem);"
563
645
  >
564
646
  {#if isHorizontalChoices}
@@ -584,10 +666,9 @@ $effect(() => {
584
666
  id={`${instanceId}-opt-${c.id}`}
585
667
  value={c.id}
586
668
  checked={
587
- (showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id
669
+ (showCorrectAnswer && isEvaluateMode ? model?.correctChoiceId : selectedId) === c.id
588
670
  }
589
- disabled={props?.model?.disabled}
590
- on:change={onRadioChange}
671
+ disabled={model?.disabled}
591
672
  class="choice-radio-bottom pie-choice-radio pie-choice-radio-bottom"
592
673
  />
593
674
  </label>
@@ -598,10 +679,9 @@ $effect(() => {
598
679
  id={`${instanceId}-opt-${c.id}`}
599
680
  value={c.id}
600
681
  checked={
601
- (showCorrectAnswer && isEvaluateMode ? props?.model?.correctChoiceId : selectedId) === c.id
682
+ (showCorrectAnswer && isEvaluateMode ? model?.correctChoiceId : selectedId) === c.id
602
683
  }
603
- disabled={props?.model?.disabled}
604
- on:change={onRadioChange}
684
+ disabled={model?.disabled}
605
685
  class="choice-radio-inline pie-choice-radio pie-choice-radio-inline"
606
686
  />
607
687
  <label for={`${instanceId}-opt-${c.id}`} class="cursor-pointer flex-1 pie-choice-label-wrap">
@@ -775,6 +855,9 @@ $effect(() => {
775
855
  .layout-audio_blank_only .audio-container,
776
856
  .layout-stimulus_image_blank .audio-container {
777
857
  display: flex;
858
+ width: min(100%, var(--mpb-audio-instructions-max-width, 875px));
859
+ margin-left: auto;
860
+ margin-right: auto;
778
861
  justify-content: flex-end;
779
862
  }
780
863
 
@@ -892,4 +975,36 @@ $effect(() => {
892
975
  grid-area: choices;
893
976
  margin-top: var(--mpb-inline-choices-margin-top, 0.25rem);
894
977
  }
978
+
979
+ /* Match Learnosity responsive behavior for CQT audio-blank layouts:
980
+ at smaller widths, audio control shifts left and answer tiles stack. */
981
+ @media (max-width: 760px) {
982
+ .layout-audio_blank_only .audio-container,
983
+ .layout-stimulus_image_blank .audio-container,
984
+ .layout-token_sequence .audio-container,
985
+ .layout-inline_sentence.has-inline-audio .audio-container {
986
+ width: 100%;
987
+ margin-left: 0;
988
+ margin-right: 0;
989
+ justify-content: flex-start;
990
+ justify-self: start;
991
+ }
992
+
993
+ .layout-audio_blank_only .pie-choices,
994
+ .layout-token_sequence .pie-choices,
995
+ .layout-stimulus_image_blank .pie-choices,
996
+ .layout-inline_sentence.has-inline-audio .pie-choices {
997
+ flex-direction: column;
998
+ align-items: flex-start;
999
+ justify-content: flex-start;
1000
+ }
1001
+
1002
+ .layout-audio_blank_only .choice-row-horizontal,
1003
+ .layout-token_sequence .choice-row-horizontal,
1004
+ .layout-stimulus_image_blank .choice-row-horizontal,
1005
+ .layout-inline_sentence.has-inline-audio .choice-row-horizontal {
1006
+ width: min(100%, var(--mpb-narrow-choice-max-width, 230px));
1007
+ align-items: flex-start;
1008
+ }
1009
+ }
895
1010
  </style>
package/src/index.iife.ts CHANGED
@@ -1,13 +1,7 @@
1
1
  /**
2
- * IIFE entry for mc-populated-blank delivery custom element.
2
+ * IIFE entry for mc-populated-blank delivery.
3
+ * Registration is handled by PIE players/loaders, not by the element bundle itself.
3
4
  */
4
5
  import Element from './delivery';
5
6
 
6
- if (typeof window !== 'undefined' && !customElements.get('mc-populated-blank-element')) {
7
- customElements.define(
8
- 'mc-populated-blank-element',
9
- Element as unknown as CustomElementConstructor
10
- );
11
- }
12
-
13
7
  export default Element;