@citolab/qti-components 7.22.1 → 7.24.0

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.
Files changed (33) hide show
  1. package/cdn/index.global.js +1 -1
  2. package/cdn/index.js +2569 -2901
  3. package/custom-elements.json +91 -208
  4. package/dist/base.js +1 -1
  5. package/dist/{chunk-NUNAE73U.js → chunk-2X7747Q4.js} +1111 -1364
  6. package/dist/chunk-2X7747Q4.js.map +1 -0
  7. package/dist/{chunk-ULXR5TWK.js → chunk-4A7ZMQKU.js} +3 -3
  8. package/dist/{chunk-SVCFKO4U.js → chunk-53QVHUUQ.js} +2 -2
  9. package/dist/{chunk-EGLWHNOX.js → chunk-BHB6PYJG.js} +4 -4
  10. package/dist/{chunk-EGLWHNOX.js.map → chunk-BHB6PYJG.js.map} +1 -1
  11. package/dist/{chunk-MF25NAZ4.js → chunk-ES2FREAP.js} +4 -4
  12. package/dist/{chunk-MF25NAZ4.js.map → chunk-ES2FREAP.js.map} +1 -1
  13. package/dist/{chunk-YAGFD5RQ.js → chunk-FEO7D54Z.js} +3 -3
  14. package/dist/chunk-FEO7D54Z.js.map +1 -0
  15. package/dist/{chunk-HCVDQUP7.js → chunk-IKBPPSNQ.js} +1516 -1696
  16. package/dist/chunk-IKBPPSNQ.js.map +1 -0
  17. package/dist/{chunk-QZLVYJDX.js → chunk-VQAG7NSK.js} +2 -2
  18. package/dist/elements.js +3 -3
  19. package/dist/index.js +8 -8
  20. package/dist/interactions.d.ts +8 -23
  21. package/dist/interactions.js +3 -3
  22. package/dist/item.css +1513 -1693
  23. package/dist/item.js +3 -3
  24. package/dist/processing.js +2 -2
  25. package/dist/qti-components-jsx.d.ts +6 -10
  26. package/dist/test.js +5 -5
  27. package/package.json +12 -11
  28. package/dist/chunk-HCVDQUP7.js.map +0 -1
  29. package/dist/chunk-NUNAE73U.js.map +0 -1
  30. package/dist/chunk-YAGFD5RQ.js.map +0 -1
  31. /package/dist/{chunk-ULXR5TWK.js.map → chunk-4A7ZMQKU.js.map} +0 -0
  32. /package/dist/{chunk-SVCFKO4U.js.map → chunk-53QVHUUQ.js.map} +0 -0
  33. /package/dist/{chunk-QZLVYJDX.js.map → chunk-VQAG7NSK.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  o as o2
3
- } from "./chunk-QZLVYJDX.js";
3
+ } from "./chunk-VQAG7NSK.js";
4
4
  import {
5
5
  E,
6
6
  Interaction,
@@ -30,7 +30,7 @@ import {
30
30
  w,
31
31
  watch,
32
32
  x
33
- } from "./chunk-EGLWHNOX.js";
33
+ } from "./chunk-BHB6PYJG.js";
34
34
  import {
35
35
  __decorateClass
36
36
  } from "./chunk-QXBXORM3.js";
@@ -363,9 +363,9 @@ var DragDropInteractionMixin = (superClass, draggablesSelector, droppablesSelect
363
363
  if (!referenceContainer || referenceContainer.clientWidth == 0) {
364
364
  return this.MAX_DRAGGABLE_WIDTH;
365
365
  }
366
- const styles = window.getComputedStyle(referenceContainer);
367
- const paddingLeft = parseFloat(styles.paddingLeft);
368
- const paddingRight = parseFloat(styles.paddingRight);
366
+ const styles3 = window.getComputedStyle(referenceContainer);
367
+ const paddingLeft = parseFloat(styles3.paddingLeft);
368
+ const paddingRight = parseFloat(styles3.paddingRight);
369
369
  return Math.min(this.MAX_DRAGGABLE_WIDTH, referenceContainer.clientWidth - paddingLeft - paddingRight);
370
370
  }
371
371
  async measureIntrinsicSize(el) {
@@ -1224,7 +1224,7 @@ var ChoicesMixin = (superClass, selector) => {
1224
1224
  this._setInputType(choice);
1225
1225
  });
1226
1226
  }
1227
- _setInputType(choiceElement) {
1227
+ async _setInputType(choiceElement) {
1228
1228
  this._internals.role = this.maxChoices === 1 ? "radiogroup" : null;
1229
1229
  if (choiceElement.internals) {
1230
1230
  const role = this.maxChoices === 1 ? "radio" : "checkbox";
@@ -1307,7 +1307,7 @@ var ChoicesMixin = (superClass, selector) => {
1307
1307
  n({ type: Number, attribute: "max-choices" })
1308
1308
  ], ChoicesMixinElement.prototype, "maxChoices", 2);
1309
1309
  __decorateClass([
1310
- watch("maxChoices", { waitUntilFirstUpdate: true })
1310
+ watch("maxChoices")
1311
1311
  ], ChoicesMixinElement.prototype, "_handleMaxChoicesChange", 1);
1312
1312
  __decorateClass([
1313
1313
  watch("disabled", { waitUntilFirstUpdate: true })
@@ -2992,24 +2992,80 @@ var e3 = class extends i3 {
2992
2992
  e3.directiveName = "unsafeHTML", e3.resultType = 1;
2993
2993
  var o3 = e2(e3);
2994
2994
 
2995
+ // ../qti-interactions/src/components/qti-inline-choice-interaction/qti-inline-choice-interaction.styles.ts
2996
+ var styles = i`
2997
+ :host {
2998
+ display: inline-block;
2999
+ vertical-align: baseline;
3000
+ position: relative;
3001
+ }
3002
+
3003
+ button[part='trigger'] {
3004
+ anchor-name: --qti-inline-choice-trigger;
3005
+ }
3006
+
3007
+ [part='value'] {
3008
+ display: inline-flex;
3009
+ align-items: center;
3010
+ gap: 0.5rem;
3011
+ min-width: 0;
3012
+ }
3013
+
3014
+ [part~='dropdown-icon'] {
3015
+ line-height: 1;
3016
+ }
3017
+
3018
+ [part='menu'] {
3019
+ position-anchor: --qti-inline-choice-trigger;
3020
+ inset: auto;
3021
+ margin: 0;
3022
+ z-index: 1000;
3023
+ top: calc(anchor(bottom) + 4px);
3024
+ left: anchor(left);
3025
+ min-width: anchor-size(width);
3026
+ max-width: min(90vw, 36rem);
3027
+ max-height: min(40vh, 20rem);
3028
+ position-try-fallbacks: flip-block, flip-inline;
3029
+ }
3030
+
3031
+ button[part~='option'] {
3032
+ width: 100%;
3033
+ }
3034
+
3035
+ [part='option-content'] {
3036
+ display: flex;
3037
+ align-items: center;
3038
+ gap: 0.5rem;
3039
+ flex-wrap: nowrap;
3040
+ white-space: nowrap;
3041
+ overflow: hidden;
3042
+ text-overflow: ellipsis;
3043
+ min-width: 0;
3044
+ }
3045
+
3046
+ button[part~='option'] img,
3047
+ button[part='trigger'] img,
3048
+ [part='menu'] img {
3049
+ display: inline-block;
3050
+ vertical-align: middle;
3051
+ }
3052
+ `;
3053
+ var qti_inline_choice_interaction_styles_default = styles;
3054
+
2995
3055
  // ../qti-interactions/src/components/qti-inline-choice-interaction/qti-inline-choice-interaction.ts
2996
- var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Interaction {
3056
+ var inlineChoiceMenuCounter = 0;
3057
+ var QtiInlineChoiceInteraction = class extends Interaction {
2997
3058
  constructor() {
2998
- super(...arguments);
3059
+ super();
2999
3060
  this.options = [];
3000
3061
  this.correctOption = "";
3001
3062
  this._dropdownOpen = false;
3002
- this._calculatedMinWidth = 0;
3003
- this._widthCalculationTimer = null;
3004
- this.dataPrompt = "";
3005
- this.#onNativeChange = (event) => {
3006
- if (this.readonly) return;
3007
- const selectedOptionValue = event.target.value;
3008
- this.#selectValue(selectedOptionValue);
3009
- };
3010
- this.#onToggleCustomDropdown = () => {
3011
- if (this.disabled || this.readonly) return;
3012
- this.#setDropdownOpen(!this._dropdownOpen);
3063
+ this._slotObserver = null;
3064
+ this._menuId = `qti-inline-choice-menu-${inlineChoiceMenuCounter++}`;
3065
+ this.#onTriggerClick = (event) => {
3066
+ if (this.disabled || this.readonly) {
3067
+ event.preventDefault();
3068
+ }
3013
3069
  };
3014
3070
  this.#onCustomTriggerKeyDown = (event) => {
3015
3071
  if (this.disabled || this.readonly) return;
@@ -3018,6 +3074,13 @@ var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Inte
3018
3074
  this.#setDropdownOpen(true);
3019
3075
  }
3020
3076
  };
3077
+ this.#onMenuToggle = (event) => {
3078
+ const toggleEvent = event;
3079
+ const open = toggleEvent.newState === "open";
3080
+ if (this._dropdownOpen !== open) {
3081
+ this._dropdownOpen = open;
3082
+ }
3083
+ };
3021
3084
  this.#onCustomMenuKeyDown = (event) => {
3022
3085
  if (!this._dropdownOpen) return;
3023
3086
  if (event.key === "Escape") {
@@ -3026,417 +3089,125 @@ var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Inte
3026
3089
  this.#focusTrigger();
3027
3090
  return;
3028
3091
  }
3029
- const optionButtons = Array.from(this.renderRoot.querySelectorAll('button[part="option"]'));
3030
- const active = this.renderRoot.activeElement;
3031
- const activeIndex = optionButtons.findIndex((btn) => btn === active);
3092
+ const optionElements = this.#allMenuOptions();
3093
+ const shadowActive = this.renderRoot.activeElement;
3094
+ const deepActive = this.#getDeepActiveElement();
3095
+ const active = shadowActive || (deepActive instanceof HTMLElement && this.#isElementInsideInteraction(deepActive) ? deepActive : null);
3096
+ const activeIndex = optionElements.findIndex((el) => el === active);
3032
3097
  if (event.key === "ArrowDown") {
3033
3098
  event.preventDefault();
3034
- optionButtons[Math.min(optionButtons.length - 1, Math.max(0, activeIndex + 1))]?.focus();
3099
+ optionElements[Math.min(optionElements.length - 1, Math.max(0, activeIndex + 1))]?.focus();
3035
3100
  }
3036
3101
  if (event.key === "ArrowUp") {
3037
3102
  event.preventDefault();
3038
- optionButtons[Math.max(0, activeIndex - 1)]?.focus();
3103
+ optionElements[Math.max(0, activeIndex - 1)]?.focus();
3104
+ }
3105
+ if (event.key === "Enter" || event.key === " ") {
3106
+ if (!active) return;
3107
+ event.preventDefault();
3108
+ if (active instanceof HTMLButtonElement) {
3109
+ active.click();
3110
+ } else {
3111
+ const value = active.getAttribute("identifier") ?? "";
3112
+ this.#selectValue(value);
3113
+ }
3039
3114
  }
3040
3115
  };
3041
- this.#onDocumentPointerDown = (event) => {
3042
- if (!this._dropdownOpen) return;
3043
- const path = event.composedPath?.();
3044
- if (path && path.includes(this)) return;
3045
- this.#setDropdownOpen(false);
3116
+ this.#onChoicesSlotChange = () => {
3117
+ this.#updateOptions();
3046
3118
  };
3047
- this.#onDocumentKeyDown = (event) => {
3048
- if (!this._dropdownOpen) return;
3049
- if (event.key !== "Escape") return;
3050
- event.preventDefault();
3051
- this.#setDropdownOpen(false);
3052
- this.#focusTrigger();
3119
+ this.#onSlottedChoiceClick = (event) => {
3120
+ if (this.disabled || this.readonly) return;
3121
+ const target = event.currentTarget;
3122
+ const value = target.getAttribute("identifier") ?? "";
3123
+ this.#selectValue(value);
3053
3124
  };
3125
+ this.internals.role = "listbox";
3054
3126
  }
3055
3127
  get isInline() {
3056
3128
  return true;
3057
3129
  }
3058
- static {
3059
- this._supportsCustomizableSelectCache = null;
3060
- }
3061
3130
  static get styles() {
3062
- return [
3063
- i`
3064
- :host {
3065
- display: inline-block;
3066
- vertical-align: baseline;
3067
- position: relative;
3068
- }
3069
-
3070
- /* --- Progressive enhancement: Customizable select (MDN / WHATWG) --- */
3071
- select[part='select'] {
3072
- font: inherit;
3073
- color: inherit;
3074
- background-color: var(--qti-bg, white);
3075
- border: var(--qti-border-thickness, 2px) var(--qti-border-style, solid) var(--qti-border-color, #c6cad0);
3076
- border-radius: var(--qti-border-radius, 0.3rem);
3077
- padding: 0.25rem 0.75rem;
3078
- min-width: var(--qti-calculated-min-width, auto);
3079
- /* Enables full styling when supported (Chromium behind a flag / rolling out). */
3080
- appearance: base-select;
3081
- }
3082
-
3083
- select[part='select']:disabled {
3084
- opacity: 0.6;
3085
- cursor: not-allowed;
3086
- }
3087
-
3088
- select[part='select']::picker(select) {
3089
- border: var(--qti-border-thickness, 2px) var(--qti-border-style, solid) var(--qti-border-color, #c6cad0);
3090
- border-radius: var(--qti-border-radius, 0.3rem);
3091
- background: var(--qti-bg, white);
3092
- box-shadow:
3093
- 0 10px 15px -3px rgb(0 0 0 / 10%),
3094
- 0 4px 6px -4px rgb(0 0 0 / 10%);
3095
- padding: 4px;
3096
- width: max-content;
3097
- min-width: 100%;
3098
- max-width: min(90vw, 36rem);
3099
- }
3100
-
3101
- select[part='select']::picker-icon {
3102
- color: var(--qti-border-color, #c6cad0);
3103
- transition: 0.4s rotate;
3104
- font-size: 1.75em;
3105
- }
3106
-
3107
- select[part='select']:open::picker-icon {
3108
- color: var(--qti-border-active, #f86d70);
3109
- rotate: 180deg;
3110
- }
3111
-
3112
- select[part='select'] > button {
3113
- font: inherit;
3114
- color: inherit;
3115
- display: inline-flex;
3116
- align-items: center;
3117
- gap: 0.25rem;
3118
- padding: 0;
3119
- background: transparent;
3120
- border: 0;
3121
- cursor: pointer;
3122
- }
3123
-
3124
- select[part='select'] selectedcontent {
3125
- display: inline-flex;
3126
- align-items: center;
3127
- gap: 0.5rem;
3128
- white-space: nowrap;
3129
- }
3130
-
3131
- option {
3132
- font: inherit;
3133
- color: inherit;
3134
- display: flex;
3135
- align-items: center;
3136
- gap: 0.5rem;
3137
- padding: 0.5rem 0.5rem;
3138
- white-space: nowrap;
3139
- line-height: 1.25;
3140
- min-height: 2.25rem;
3141
- }
3142
-
3143
- option:hover {
3144
- background-color: var(--qti-hover-bg, #f9fafb);
3145
- }
3146
-
3147
- option:checked {
3148
- background-color: var(--qti-bg-active, #ffecec);
3149
- }
3150
-
3151
- option::checkmark {
3152
- color: var(--qti-border-active, #f86d70);
3153
- }
3154
-
3155
- /* --- Fallback custom listbox (for browsers without customizable select) --- */
3156
- button[part='trigger'] {
3157
- font: inherit;
3158
- color: inherit;
3159
- background-color: var(--qti-bg, white);
3160
- cursor: pointer;
3161
- display: inline-flex;
3162
- align-items: center;
3163
- gap: 0.5rem;
3164
- justify-content: space-between;
3165
- border: var(--qti-border-thickness, 2px) var(--qti-border-style, solid) var(--qti-border-color, #c6cad0);
3166
- border-radius: var(--qti-border-radius, 0.3rem);
3167
- padding: 0.25rem 0.75rem;
3168
- min-width: var(--qti-calculated-min-width, auto);
3169
- }
3170
-
3171
- [part='value'] {
3172
- display: inline-flex;
3173
- align-items: center;
3174
- gap: 0.5rem;
3175
- min-width: 0;
3176
- }
3177
-
3178
- [part='dropdown-icon'] {
3179
- display: inline-flex;
3180
- align-items: center;
3181
- justify-content: center;
3182
- flex: 0 0 auto;
3183
- transition: transform 150ms ease;
3184
- transform-origin: 50% 50%;
3185
- color: var(--qti-border-color, #c6cad0);
3186
- font-size: 1.75em;
3187
- line-height: 1;
3188
- }
3189
-
3190
- button[part='trigger'][aria-expanded='true'] [part='dropdown-icon'] {
3191
- transform: rotate(180deg);
3192
- color: var(--qti-border-active, #f86d70);
3193
- }
3194
-
3195
- button[part='trigger'][disabled] {
3196
- cursor: not-allowed;
3197
- opacity: 0.6;
3198
- }
3199
-
3200
- [part='menu'] {
3201
- position: absolute;
3202
- z-index: 1000;
3203
- top: calc(100% + 4px);
3204
- left: 0;
3205
- min-width: 100%;
3206
- max-width: min(90vw, 36rem);
3207
- max-height: min(40vh, 20rem);
3208
- overflow: auto;
3209
- background-color: var(--qti-bg, white);
3210
- border: var(--qti-border-thickness, 2px) var(--qti-border-style, solid) var(--qti-border-color, #c6cad0);
3211
- border-radius: var(--qti-border-radius, 0.3rem);
3212
- box-shadow:
3213
- 0 10px 15px -3px rgb(0 0 0 / 10%),
3214
- 0 4px 6px -4px rgb(0 0 0 / 10%);
3215
- padding: 4px;
3216
- box-sizing: border-box;
3217
- transform: translate(var(--qti-menu-shift-x, 0px), var(--qti-menu-shift-y, 0px));
3218
- }
3219
-
3220
- [part='menu'][data-placement='top'] {
3221
- top: auto;
3222
- bottom: calc(100% + 4px);
3223
- }
3224
-
3225
- button[part='option'] {
3226
- font: inherit;
3227
- color: inherit;
3228
- background-color: transparent;
3229
- border: 0;
3230
- padding: 0.5rem 0.5rem;
3231
- width: 100%;
3232
- text-align: left;
3233
- border-radius: calc(var(--qti-border-radius, 0.3rem) - 2px);
3234
- cursor: pointer;
3235
- white-space: nowrap;
3236
- overflow: hidden;
3237
- text-overflow: ellipsis;
3238
- line-height: 1.25;
3239
- min-height: 2.25rem;
3240
- }
3241
-
3242
- button[part='option'][aria-selected='true'] {
3243
- background-color: var(--qti-bg-active, #ffecec);
3244
- }
3245
-
3246
- button[part='option']:hover {
3247
- background-color: var(--qti-hover-bg, #f9fafb);
3248
- }
3249
-
3250
- button[part='option']:focus-visible {
3251
- outline: 2px solid var(--qti-border-active, #f86d70);
3252
- outline-offset: 2px;
3253
- }
3254
-
3255
- [part='option-content'] {
3256
- display: flex;
3257
- align-items: center;
3258
- gap: 0.5rem;
3259
- flex-wrap: nowrap;
3260
- white-space: nowrap;
3261
- overflow: hidden;
3262
- text-overflow: ellipsis;
3263
- min-width: 0;
3264
- }
3265
-
3266
- select[part='select'] img,
3267
- button[part='option'] img,
3268
- button[part='trigger'] img,
3269
- [part='menu'] img {
3270
- display: inline-block;
3271
- max-height: 1em;
3272
- max-width: 1.5em;
3273
- vertical-align: middle;
3274
- }
3275
- `
3276
- ];
3277
- }
3278
- static {
3279
- this.inputWidthClass = [
3280
- "",
3281
- "qti-input-width-2",
3282
- "qti-input-width-1",
3283
- "qti-input-width-3",
3284
- "qti-input-width-4",
3285
- "qti-input-width-6",
3286
- "qti-input-width-10",
3287
- "qti-input-width-15",
3288
- "qti-input-width-20",
3289
- "qti-input-width-72"
3290
- ];
3131
+ return [qti_inline_choice_interaction_styles_default];
3291
3132
  }
3292
3133
  render() {
3293
3134
  const selected = this.#selectedOption();
3294
- const useCustomizableSelect = this.#supportsCustomizableSelect();
3295
3135
  return x`
3296
- ${useCustomizableSelect ? x`
3297
- <select
3298
- part="select"
3299
- @change=${this.#onNativeChange}
3300
- ?disabled="${this.disabled || this.readonly}"
3301
- .value="${selected?.value ?? ""}"
3302
- >
3303
- <button type="button">
3304
- <selectedcontent></selectedcontent>
3305
- </button>
3306
- ${this.options.map(
3307
- (option) => x`<option value="${option.value}">${o3(option.textContent)}</option>`
3308
- )}
3309
- </select>
3310
- ` : x`
3311
- <button
3312
- part="trigger"
3313
- type="button"
3314
- @click=${this.#onToggleCustomDropdown}
3315
- @keydown=${this.#onCustomTriggerKeyDown}
3316
- aria-haspopup="listbox"
3317
- aria-expanded="${this._dropdownOpen ? "true" : "false"}"
3318
- ?disabled="${this.disabled}"
3319
- data-readonly="${this.readonly ? "true" : "false"}"
3320
- >
3321
- <span part="value">${o3(selected?.textContent ?? "")}</span>
3322
- <span part="dropdown-icon" aria-hidden="true">▾</span>
3323
- </button>
3324
- ${this._dropdownOpen ? x`
3325
- <div part="menu" role="listbox" @keydown=${this.#onCustomMenuKeyDown}>
3326
- ${this.options.map(
3327
- (option) => x`
3328
- <button
3329
- part="option"
3330
- type="button"
3331
- role="option"
3332
- aria-selected="${option.selected ? "true" : "false"}"
3333
- @click="${() => this.#selectValue(option.value)}"
3334
- >
3335
- <span part="option-content">${o3(option.textContent)}</span>
3336
- </button>
3337
- `
3338
- )}
3339
- </div>
3340
- ` : null}
3341
- `}
3136
+ <button
3137
+ part="trigger"
3138
+ type="button"
3139
+ @click=${this.#onTriggerClick}
3140
+ @keydown=${this.#onCustomTriggerKeyDown}
3141
+ aria-haspopup="listbox"
3142
+ aria-expanded="${this._dropdownOpen ? "true" : "false"}"
3143
+ aria-controls="${this._menuId}"
3144
+ popovertarget="${this._menuId}"
3145
+ popovertargetaction="toggle"
3146
+ ?disabled="${this.disabled}"
3147
+ data-readonly="${this.readonly ? "true" : "false"}"
3148
+ >
3149
+ <span part="value">${o3(selected?.textContent ?? "")}</span>
3150
+ <span part="${this._dropdownOpen ? "dropdown-icon dropdown-icon-open" : "dropdown-icon"}" aria-hidden="true"
3151
+ >▾</span
3152
+ >
3153
+ </button>
3154
+ <div
3155
+ id="${this._menuId}"
3156
+ part="menu"
3157
+ role="listbox"
3158
+ popover="auto"
3159
+ @toggle=${this.#onMenuToggle}
3160
+ @keydown=${this.#onCustomMenuKeyDown}
3161
+ >
3162
+ <button
3163
+ part="${this.options[0]?.selected ? "option option-prompt option-selected" : "option option-prompt"}"
3164
+ type="button"
3165
+ role="option"
3166
+ aria-selected="${this.options[0]?.selected ? "true" : "false"}"
3167
+ @click=${() => this.#selectValue("")}
3168
+ >
3169
+ <span part="option-content">${o3(this.options[0]?.textContent ?? "")}</span>
3170
+ </button>
3171
+ <slot @slotchange=${this.#onChoicesSlotChange}></slot>
3172
+ </div>
3342
3173
  ${o3(this.correctOption)}
3343
3174
  `;
3344
3175
  }
3345
- connectedCallback() {
3176
+ async connectedCallback() {
3346
3177
  super.connectedCallback();
3347
3178
  this.#updateOptions();
3348
- if (!this.#supportsCustomizableSelect()) {
3349
- document.addEventListener("pointerdown", this.#onDocumentPointerDown, true);
3350
- document.addEventListener("keydown", this.#onDocumentKeyDown, true);
3351
- }
3352
- this._estimateOptimalWidth();
3179
+ this.#startSlotObserver();
3180
+ await this.updateComplete;
3181
+ this.#estimateOptimalWidth();
3353
3182
  }
3354
3183
  disconnectedCallback() {
3355
3184
  super.disconnectedCallback();
3356
- if (!this.#supportsCustomizableSelect()) {
3357
- document.removeEventListener("pointerdown", this.#onDocumentPointerDown, true);
3358
- document.removeEventListener("keydown", this.#onDocumentKeyDown, true);
3359
- }
3360
- if (this._widthCalculationTimer !== null) {
3361
- window.clearTimeout(this._widthCalculationTimer);
3362
- this._widthCalculationTimer = null;
3363
- }
3185
+ this.#teardownSlottedChoices();
3186
+ this._slotObserver?.disconnect();
3187
+ this._slotObserver = null;
3364
3188
  }
3365
3189
  willUpdate(changed) {
3366
- if (changed.has("configContext") || changed.has("dataPrompt")) {
3190
+ if (changed.has("configContext")) {
3367
3191
  this.#updateOptions();
3368
3192
  }
3369
3193
  }
3370
3194
  updated(changed) {
3371
3195
  const dropdownOpenKey = "_dropdownOpen";
3372
3196
  if (changed.has(dropdownOpenKey) && this._dropdownOpen) {
3373
- this.#positionCustomMenu();
3374
- const selected = this.renderRoot.querySelector('button[part="option"][aria-selected="true"]');
3375
- selected?.focus();
3197
+ this.#syncSlottedChoices();
3198
+ const first = this.#allMenuOptions()[0];
3199
+ first?.focus();
3200
+ }
3201
+ if (changed.has("disabled") || changed.has("readonly")) {
3202
+ this.#syncSlottedChoices();
3376
3203
  }
3377
3204
  }
3378
3205
  #selectedOption() {
3379
3206
  return this.options.find((option) => option.selected) ?? this.options[0];
3380
3207
  }
3381
- /**
3382
- * Progressive enhancement for "customizable select" (WHATWG / MDN: `appearance: base-select` + `::picker()`).
3383
- *
3384
- * Notes on current browser behavior (observed around Feb 2026):
3385
- * - Chromium-based browsers can support customizable select in light DOM, but it does not reliably work when the
3386
- * `<select>` lives inside a shadow root (e.g. the internal `<button>/<selectedcontent>` can end up effectively
3387
- * not rendered, so rich content like images disappears).
3388
- * - Firefox support is not generally available yet, so we fall back to our custom listbox there as well.
3389
- *
3390
- * Because `CSS.supports(...)` may return syntax-only true, we do a final DOM probe to ensure the customizable-select
3391
- * markup actually takes effect in the current environment before opting in.
3392
- */
3393
- #supportsCustomizableSelect() {
3394
- if (_QtiInlineChoiceInteraction._supportsCustomizableSelectCache !== null) {
3395
- return _QtiInlineChoiceInteraction._supportsCustomizableSelectCache;
3396
- }
3397
- if (typeof CSS === "undefined" || typeof CSS.supports !== "function") {
3398
- _QtiInlineChoiceInteraction._supportsCustomizableSelectCache = false;
3399
- return false;
3400
- }
3401
- const supportsPickerSelector = CSS.supports("selector(::picker(select))") || CSS.supports("selector(select::picker(select))");
3402
- const supportsAppearanceValue = CSS.supports("appearance: base-select") || CSS.supports("-webkit-appearance: base-select");
3403
- if (!supportsPickerSelector || !supportsAppearanceValue) {
3404
- _QtiInlineChoiceInteraction._supportsCustomizableSelectCache = false;
3405
- return false;
3406
- }
3407
- try {
3408
- const container = document.createElement("div");
3409
- container.style.position = "absolute";
3410
- container.style.top = "-9999px";
3411
- container.style.left = "-9999px";
3412
- const select = document.createElement("select");
3413
- select.style.appearance = "base-select";
3414
- select.style.webkitAppearance = "base-select";
3415
- const button = document.createElement("button");
3416
- button.type = "button";
3417
- const selected = document.createElement("selectedcontent");
3418
- selected.textContent = "probe";
3419
- button.appendChild(selected);
3420
- const option = document.createElement("option");
3421
- option.value = "probe";
3422
- option.textContent = "probe";
3423
- select.appendChild(button);
3424
- select.appendChild(option);
3425
- container.appendChild(select);
3426
- (document.body || document.documentElement).appendChild(container);
3427
- const rect = button.getBoundingClientRect();
3428
- container.remove();
3429
- const supported = rect.width > 0 && rect.height > 0;
3430
- _QtiInlineChoiceInteraction._supportsCustomizableSelectCache = supported;
3431
- return supported;
3432
- } catch {
3433
- _QtiInlineChoiceInteraction._supportsCustomizableSelectCache = false;
3434
- return false;
3435
- }
3436
- }
3437
3208
  #updateOptions() {
3438
3209
  const choices = Array.from(this.querySelectorAll("qti-inline-choice"));
3439
- const prompt = this.dataPrompt || this.configContext?.inlineChoicePrompt || "select";
3210
+ const prompt = this.dataset.prompt || this.configContext?.inlineChoicePrompt || "select";
3440
3211
  const currentlySelectedValue = this.options.find((o6) => o6.selected)?.value ?? "";
3441
3212
  const nextOptions = [
3442
3213
  {
@@ -3455,22 +3226,31 @@ var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Inte
3455
3226
  ];
3456
3227
  const hasSelected = nextOptions.some((o6) => o6.selected);
3457
3228
  this.options = hasSelected ? nextOptions : nextOptions.map((o6, i5) => ({ ...o6, selected: i5 === 0 }));
3458
- this._estimateOptimalWidth();
3229
+ this.#syncSlottedChoices();
3230
+ this.#estimateOptimalWidth();
3459
3231
  }
3460
- /**
3461
- * Simple width estimation based on text content length - no DOM manipulation needed
3462
- */
3463
- _estimateOptimalWidth() {
3464
- if (this.options.length === 0) return;
3465
- let maxLength = 0;
3466
- for (const option of this.options) {
3467
- const textContent = option.textContent.replace(/<[^>]*>/g, "").trim();
3468
- maxLength = Math.max(maxLength, textContent.length);
3469
- }
3470
- const estimatedEm = Math.max(maxLength * 0.6 + 4, 8.75);
3471
- const maxEm = Math.min(estimatedEm, 40);
3472
- this._calculatedMinWidth = maxEm;
3473
- this.style.setProperty("--qti-calculated-min-width", `${maxEm}em`);
3232
+ #estimateOptimalWidth() {
3233
+ const menu = this.#menuElement();
3234
+ const trigger = this.renderRoot.querySelector('button[part="trigger"]');
3235
+ if (!menu || !trigger) return;
3236
+ const dropdownIcon = trigger.querySelector('span[part~="dropdown-icon"]');
3237
+ const iconWidth = dropdownIcon ? dropdownIcon.getBoundingClientRect().width : 0;
3238
+ const wasOpen = menu.matches(":popover-open");
3239
+ const prevVisibility = menu.style.visibility;
3240
+ const prevDisplay = menu.style.display;
3241
+ if (!wasOpen) {
3242
+ menu.style.visibility = "hidden";
3243
+ menu.style.display = "block";
3244
+ menu.showPopover();
3245
+ }
3246
+ const rectWidth = menu.getBoundingClientRect().width;
3247
+ const widthPx = Math.max(rectWidth, menu.scrollWidth);
3248
+ if (!wasOpen) {
3249
+ menu.hidePopover();
3250
+ menu.style.visibility = prevVisibility;
3251
+ menu.style.display = prevDisplay;
3252
+ }
3253
+ trigger.style.width = `${widthPx + iconWidth}px`;
3474
3254
  }
3475
3255
  validate() {
3476
3256
  const selectedOption = this.options.find((option) => option.selected);
@@ -3479,10 +3259,12 @@ var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Inte
3479
3259
  reset() {
3480
3260
  this.#setDropdownOpen(false);
3481
3261
  this.options = this.options.map((option, i5) => ({ ...option, selected: i5 === 0 }));
3262
+ this.#syncSlottedChoices();
3482
3263
  }
3483
3264
  set response(value) {
3484
3265
  const nextValue = value ?? "";
3485
3266
  this.options = this.options.map((option) => ({ ...option, selected: option.value === nextValue }));
3267
+ this.#syncSlottedChoices();
3486
3268
  }
3487
3269
  get response() {
3488
3270
  const value = this.options.find((option) => option.selected)?.value ?? "";
@@ -3502,74 +3284,107 @@ var _QtiInlineChoiceInteraction = class _QtiInlineChoiceInteraction extends Inte
3502
3284
  }
3503
3285
  this.correctOption = `<span part="correct-option" style="border:1px solid var(--qti-correct); border-radius:4px; padding: 2px 4px; margin: 4px; display:inline-block">${correctOptionData.textContent}</span>`;
3504
3286
  }
3505
- #onNativeChange;
3506
3287
  #selectValue(value) {
3507
3288
  this.options = this.options.map((option) => ({ ...option, selected: option.value === value }));
3289
+ this.#syncSlottedChoices();
3508
3290
  this.saveResponse(value);
3509
3291
  this.#setDropdownOpen(false);
3510
3292
  }
3511
3293
  #setDropdownOpen(open) {
3512
- if (this._dropdownOpen === open) return;
3513
- this._dropdownOpen = open;
3294
+ const menu = this.#menuElement();
3295
+ if (!menu) return;
3296
+ if (open) {
3297
+ if (!menu.matches(":popover-open")) {
3298
+ menu.showPopover();
3299
+ }
3300
+ return;
3301
+ }
3302
+ if (menu.matches(":popover-open")) {
3303
+ menu.hidePopover();
3304
+ }
3514
3305
  }
3515
- #onToggleCustomDropdown;
3306
+ #onTriggerClick;
3516
3307
  #onCustomTriggerKeyDown;
3308
+ #onMenuToggle;
3517
3309
  #onCustomMenuKeyDown;
3518
3310
  #focusTrigger() {
3519
3311
  this.renderRoot.querySelector('button[part="trigger"]')?.focus();
3520
3312
  }
3521
- #onDocumentPointerDown;
3522
- #onDocumentKeyDown;
3523
- #positionCustomMenu() {
3524
- if (!this._dropdownOpen) return;
3525
- const menu = this.renderRoot.querySelector('[part="menu"]');
3526
- const trigger = this.renderRoot.querySelector('button[part="trigger"]');
3527
- if (!menu || !trigger) return;
3528
- menu.dataset.placement = "bottom";
3529
- menu.style.setProperty("--qti-menu-shift-x", "0px");
3530
- menu.style.setProperty("--qti-menu-shift-y", "0px");
3531
- menu.style.left = "0px";
3532
- menu.style.right = "auto";
3533
- const viewportWidth = document.documentElement?.clientWidth || window.innerWidth;
3534
- const viewportHeight = document.documentElement?.clientHeight || window.innerHeight;
3535
- const margin = 8;
3536
- const triggerRect = trigger.getBoundingClientRect();
3537
- const maxWidth = Math.max(0, viewportWidth - margin * 2);
3538
- menu.style.maxWidth = `${maxWidth}px`;
3539
- menu.style.minWidth = `${Math.min(triggerRect.width, maxWidth)}px`;
3540
- let menuRect = menu.getBoundingClientRect();
3541
- const spaceBelow = viewportHeight - triggerRect.bottom;
3542
- const spaceAbove = triggerRect.top;
3543
- if (menuRect.bottom > viewportHeight - margin && spaceAbove > spaceBelow) {
3544
- menu.dataset.placement = "top";
3545
- menuRect = menu.getBoundingClientRect();
3546
- }
3547
- const maxLeft = Math.max(margin, viewportWidth - margin - menuRect.width);
3548
- const desiredLeft = Math.min(maxLeft, Math.max(margin, triggerRect.left));
3549
- const offsetLeft = desiredLeft - triggerRect.left;
3550
- menu.style.left = `${offsetLeft}px`;
3313
+ #getDeepActiveElement() {
3314
+ let current = document.activeElement;
3315
+ while (current && current instanceof HTMLElement && current.shadowRoot?.activeElement) {
3316
+ current = current.shadowRoot.activeElement;
3317
+ }
3318
+ return current;
3319
+ }
3320
+ #isElementInsideInteraction(element) {
3321
+ return element === this || this.contains(element) || this.renderRoot.contains(element);
3322
+ }
3323
+ #menuElement() {
3324
+ return this.renderRoot.querySelector(`#${this._menuId}`);
3325
+ }
3326
+ #startSlotObserver() {
3327
+ this._slotObserver = new MutationObserver(() => this.#updateOptions());
3328
+ this._slotObserver.observe(this, { childList: true, subtree: true });
3329
+ }
3330
+ #onChoicesSlotChange;
3331
+ #onSlottedChoiceClick;
3332
+ #teardownSlottedChoices() {
3333
+ const choices = Array.from(this.querySelectorAll("qti-inline-choice"));
3334
+ for (const choice of choices) {
3335
+ choice.removeEventListener("click", this.#onSlottedChoiceClick);
3336
+ choice.removeAttribute("tabindex");
3337
+ choice.internals.role = null;
3338
+ choice.internals.ariaSelected = null;
3339
+ choice.internals.ariaChecked = "false";
3340
+ choice.internals.states.delete("--checked");
3341
+ }
3342
+ }
3343
+ async #syncSlottedChoices() {
3344
+ await this.updateComplete;
3345
+ const selectedValue = this.options.find((option) => option.selected)?.value ?? "";
3346
+ const choices = Array.from(this.querySelectorAll("qti-inline-choice"));
3347
+ for (const choice of choices) {
3348
+ const value = choice.getAttribute("identifier") ?? "";
3349
+ const isSelected = value === selectedValue;
3350
+ choice.removeEventListener("click", this.#onSlottedChoiceClick);
3351
+ choice.addEventListener("click", this.#onSlottedChoiceClick);
3352
+ choice.disabled = this.disabled;
3353
+ choice.readonly = this.readonly;
3354
+ choice.internals.role = "option";
3355
+ choice.internals.ariaSelected = isSelected ? "true" : "false";
3356
+ choice.internals.ariaChecked = isSelected ? "true" : "false";
3357
+ choice.internals.ariaDisabled = this.disabled ? "true" : "false";
3358
+ choice.internals.ariaReadOnly = this.readonly ? "true" : "false";
3359
+ choice.removeAttribute("aria-disabled");
3360
+ choice.removeAttribute("aria-readonly");
3361
+ if (isSelected) {
3362
+ choice.internals.states.add("--checked");
3363
+ } else {
3364
+ choice.internals.states.delete("--checked");
3365
+ }
3366
+ choice.tabIndex = -1;
3367
+ }
3368
+ }
3369
+ #allMenuOptions() {
3370
+ const promptOption = this.renderRoot.querySelector('button[part~="option"]');
3371
+ const slottedChoices = Array.from(this.querySelectorAll("qti-inline-choice"));
3372
+ return [...promptOption ? [promptOption] : [], ...slottedChoices];
3551
3373
  }
3552
3374
  };
3553
3375
  __decorateClass([
3554
3376
  r()
3555
- ], _QtiInlineChoiceInteraction.prototype, "options", 2);
3556
- __decorateClass([
3557
- r()
3558
- ], _QtiInlineChoiceInteraction.prototype, "correctOption", 2);
3377
+ ], QtiInlineChoiceInteraction.prototype, "options", 2);
3559
3378
  __decorateClass([
3560
3379
  r()
3561
- ], _QtiInlineChoiceInteraction.prototype, "_dropdownOpen", 2);
3380
+ ], QtiInlineChoiceInteraction.prototype, "correctOption", 2);
3562
3381
  __decorateClass([
3563
3382
  r()
3564
- ], _QtiInlineChoiceInteraction.prototype, "_calculatedMinWidth", 2);
3565
- __decorateClass([
3566
- n({ attribute: "data-prompt", type: String })
3567
- ], _QtiInlineChoiceInteraction.prototype, "dataPrompt", 2);
3383
+ ], QtiInlineChoiceInteraction.prototype, "_dropdownOpen", 2);
3568
3384
  __decorateClass([
3569
3385
  c({ context: configContext, subscribe: true }),
3570
3386
  n({ attribute: false })
3571
- ], _QtiInlineChoiceInteraction.prototype, "configContext", 2);
3572
- var QtiInlineChoiceInteraction = _QtiInlineChoiceInteraction;
3387
+ ], QtiInlineChoiceInteraction.prototype, "configContext", 2);
3573
3388
 
3574
3389
  // ../qti-interactions/src/components/qti-match-interaction/qti-match-interaction.styles.ts
3575
3390
  var qti_match_interaction_styles_default = i`
@@ -4609,94 +4424,6 @@ var QtiPortableCustomInteraction = class extends Interaction {
4609
4424
  }
4610
4425
  return unescaped;
4611
4426
  }
4612
- /**
4613
- * Resolve stylesheet href against baseUrl or document origin
4614
- */
4615
- resolveStylesheetHref(href) {
4616
- if (!href) return href;
4617
- if (href.startsWith("http://") || href.startsWith("https://")) {
4618
- return href;
4619
- }
4620
- if (href.startsWith("//")) {
4621
- return `${window.location.protocol}${href}`;
4622
- }
4623
- const base = this.baseUrl && this.baseUrl.length > 0 ? this.baseUrl.startsWith("http") || this.baseUrl.startsWith("blob") || this.baseUrl.startsWith("base64") ? this.baseUrl : removeDoubleSlashes(`${window.location.origin}${this.baseUrl}`) : window.location.origin;
4624
- const normalizedBase = base.endsWith("/") ? base : `${base}/`;
4625
- try {
4626
- return new URL(href, normalizedBase).toString();
4627
- } catch {
4628
- return href;
4629
- }
4630
- }
4631
- /**
4632
- * Collect qti-stylesheet elements for iframe injection
4633
- */
4634
- getStylesheetConfigs() {
4635
- const stylesheets = this.getDirectChildrenByTag("qti-stylesheet");
4636
- if (!stylesheets.length) return [];
4637
- return stylesheets.map((el, index) => {
4638
- const href = el.getAttribute("href");
4639
- if (href) {
4640
- const resolved = this.resolveStylesheetHref(href);
4641
- return { href: resolved, scoped: false, key: resolved };
4642
- }
4643
- const content = el.textContent?.trim();
4644
- if (content) {
4645
- return { content, scoped: false, key: `inline-${index}` };
4646
- }
4647
- return null;
4648
- }).filter(Boolean);
4649
- }
4650
- getSharedStylesheetContent() {
4651
- let cssText = "";
4652
- const seen = /* @__PURE__ */ new Set();
4653
- const sheets = Array.from(document.styleSheets || []);
4654
- for (const sheet of sheets) {
4655
- try {
4656
- if (sheet.href && !sheet.href.startsWith(window.location.origin)) {
4657
- continue;
4658
- }
4659
- const ownerNode = sheet.ownerNode;
4660
- if (ownerNode && ownerNode.tagName === "STYLE") {
4661
- const text = ownerNode.textContent || "";
4662
- if (text && !seen.has(text)) {
4663
- cssText += `${text}
4664
- `;
4665
- seen.add(text);
4666
- }
4667
- continue;
4668
- }
4669
- const rules = sheet.cssRules ? Array.from(sheet.cssRules) : [];
4670
- if (rules.length) {
4671
- const text = rules.map((rule) => rule.cssText).join("\n");
4672
- if (text && !seen.has(text)) {
4673
- cssText += `${text}
4674
- `;
4675
- seen.add(text);
4676
- }
4677
- }
4678
- } catch {
4679
- }
4680
- }
4681
- const trimmed = cssText.trim();
4682
- return trimmed.length ? trimmed : null;
4683
- }
4684
- getSharedStylesheetConfig() {
4685
- const content = this.getSharedStylesheetContent();
4686
- if (!content) return null;
4687
- return { content, scoped: false, key: "__qti_shared_css__" };
4688
- }
4689
- /**
4690
- * IFRAME MODE: Add stylesheets to iframe
4691
- */
4692
- #addStylesheetsToIframe() {
4693
- const stylesheets = this.getStylesheetConfigs();
4694
- const shared = this.getSharedStylesheetConfig();
4695
- const payload = shared ? [shared, ...stylesheets] : stylesheets;
4696
- if (payload.length > 0) {
4697
- this.sendMessageToIframe("setStylesheets", payload);
4698
- }
4699
- }
4700
4427
  disconnectedCallback() {
4701
4428
  super.disconnectedCallback();
4702
4429
  window.removeEventListener("message", this.handleIframeMessage);
@@ -4759,7 +4486,6 @@ var QtiPortableCustomInteraction = class extends Interaction {
4759
4486
  this._iframeObjectUrl = null;
4760
4487
  }
4761
4488
  this.#addMarkupToIframe();
4762
- this.#addStylesheetsToIframe();
4763
4489
  this.#sendIframeInitData();
4764
4490
  };
4765
4491
  const iframeName = `qti-pci-${this.responseIdentifier}-${Date.now()}`;
@@ -4816,13 +4542,18 @@ var QtiPortableCustomInteraction = class extends Interaction {
4816
4542
  return modules;
4817
4543
  }
4818
4544
  /**
4819
- * IFRAME MODE: Add markup and properties to iframe
4545
+ * IFRAME MODE: Add markup, stylesheets, and properties to iframe
4820
4546
  */
4821
4547
  #addMarkupToIframe() {
4822
4548
  const markup = this.querySelector("qti-interaction-markup");
4823
4549
  if (markup) {
4824
4550
  this.sendMessageToIframe("setMarkup", markup.innerHTML);
4825
4551
  }
4552
+ const stylesheets = Array.from(this.querySelectorAll("qti-stylesheet"));
4553
+ if (stylesheets.length) {
4554
+ const stylesheetsHtml = stylesheets.map((sheet) => sheet.outerHTML).join("\n");
4555
+ this.sendMessageToIframe("setStylesheets", stylesheetsHtml);
4556
+ }
4826
4557
  const properties = this.querySelector("properties");
4827
4558
  if (properties) {
4828
4559
  this.sendMessageToIframe("setProperties", properties.innerHTML);
@@ -4855,863 +4586,873 @@ var QtiPortableCustomInteraction = class extends Interaction {
4855
4586
  font-weight: ${parentStyles.getPropertyValue("font-weight")};
4856
4587
  color: ${parentStyles.getPropertyValue("color")};
4857
4588
  `;
4858
- return `<!DOCTYPE html>
4859
- <html lang="en">
4860
- <head>
4861
- <meta charset="utf-8" />
4862
- <title>QTI PCI Container</title>
4863
- <base href="${window.location.origin}" />
4864
- <style>
4865
- body, html {
4866
- margin: 0;
4867
- padding: 0;
4868
- width: 100%;
4869
- height: auto;
4870
- overflow: hidden;
4871
- /* Add the extracted font styles here */
4872
- ${fontStyles}
4873
- }
4874
- .qti-customInteraction {
4875
- width: 100%;
4876
- height: 100%;
4877
- }
4878
- #pci-container {
4879
- width: 100%;
4880
- }
4881
- qti-interaction-markup {
4882
- display: block;
4883
- width: 100%;
4884
- min-height: 50px;
4885
- }
4886
- </style>
4887
- <script src="${this.requireJsUrl}"></script>
4888
- <script>
4889
- const forwardConsole = ${forwardConsole ? "true" : "false"};
4890
- if (forwardConsole) {
4891
- const originalLog = console.log.bind(console);
4892
- const originalError = console.error.bind(console);
4893
- const stringifyArgs = args =>
4894
- args.map(arg => {
4895
- if (typeof arg === 'string') return arg;
4896
- try {
4897
- return JSON.stringify(arg);
4898
- } catch (e) {
4899
- return String(arg);
4900
- }
4901
- });
4902
- console.log = (...args) => {
4903
- originalLog(...args);
4904
- window.parent.postMessage(
4905
- {
4906
- source: 'qti-pci-iframe',
4907
- responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4908
- method: 'console',
4909
- level: 'log',
4910
- args: stringifyArgs(args)
4911
- },
4912
- '*'
4913
- );
4914
- };
4915
- console.error = (...args) => {
4916
- originalError(...args);
4917
- window.parent.postMessage(
4918
- {
4919
- source: 'qti-pci-iframe',
4920
- responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4921
- method: 'console',
4922
- level: 'error',
4923
- args: stringifyArgs(args)
4924
- },
4925
- '*'
4926
- );
4927
- };
4928
- }
4929
- // Define standard paths and shims
4930
- window.requirePaths = ${requirePaths};
4931
-
4932
- window.requireShim = ${requireShim};
4933
-
4934
- // Single initial RequireJS configuration with error handling
4935
- window.requirejs.config({
4936
- catchError: true,
4937
- waitSeconds: 30,
4938
- paths: window.requirePaths,
4939
- baseUrl: '${iframeBaseUrl}',
4940
- shim: window.requireShim,
4941
- onNodeCreated: function(node, config, moduleName, url) {
4942
- // Add error handler to script node
4943
- node.addEventListener('error', function(evt) {
4944
- console.error('Script load error for module:', moduleName, 'URL:', url, 'Event:', evt);
4945
- });
4946
- },
4947
- onError: function(err) {
4948
- console.error('RequireJS error:', {
4949
- type: err.requireType,
4950
- modules: err.requireModules,
4951
- error: err
4952
- });
4953
-
4954
- if (err.requireType === 'scripterror') {
4955
- console.error('Script error usually indicates a network or CORS issue with:', err.requireModules);
4956
- }
4957
-
4958
- // Notify parent window about the error
4959
- window.parent.postMessage({
4960
- source: 'qti-pci-iframe',
4961
- responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4962
- method: 'error',
4963
- params: {
4964
- message: 'RequireJS ' + err.requireType + ' error for modules: ' + err.requireModules,
4965
- details: {
4966
- type: err.requireType,
4967
- modules: err.requireModules,
4968
- error: err.toString()
4589
+ return (
4590
+ /* html */
4591
+ `<!DOCTYPE html>
4592
+ <html lang="en">
4593
+ <head>
4594
+ <meta charset="utf-8" />
4595
+ <title>QTI PCI Container</title>
4596
+ <base href="${window.location.origin}" />
4597
+ <script type="module">
4598
+ import 'https://unpkg.com/@citolab/qti-components/cdn';
4599
+ </script>
4600
+ <style>
4601
+ body, html {
4602
+ margin: 0;
4603
+ padding: 0;
4604
+ width: 100%;
4605
+ height: auto;
4606
+ overflow: hidden;
4607
+ /* Add the extracted font styles here */
4608
+ ${fontStyles}
4609
+ };
4610
+ .qti-customInteraction {
4611
+ width: 100%;
4612
+ height: 100%;
4969
4613
  }
4970
- }
4971
- }, '*');
4972
- }
4973
- });
4974
-
4975
- // PCI Manager for iframe implementation
4976
- window.PCIManager = {
4977
- pciInstance: null,
4978
- container: null,
4979
- markupEl: null,
4980
- propertiesEl: null,
4981
- customInteractionTypeIdentifier: null,
4982
- responseIdentifier: null,
4983
- pendingBoundTo: null,
4984
- pendingMarkup: null,
4985
- pendingProperties: null,
4986
- pendingState: null,
4987
- pendingStylesheets: null,
4988
- stylesheetKeys: {},
4989
- interactionChangedViaEvent: false,
4990
- eventBridgeAttached: false,
4991
- lastResponseStr: null,
4992
- hadResponse: false,
4993
-
4994
- initialize: function(config) {
4995
- this.customInteractionTypeIdentifier = config.customInteractionTypeIdentifier;
4996
- this.responseIdentifier = config.responseIdentifier;
4997
- this.container = document.getElementById('pci-container');
4998
- this.container.classList.add('qti-customInteraction');
4999
-
5000
- function qtiVariableHasValue(qtiVar) {
5001
- if (!qtiVar) return false;
5002
- if (qtiVar.base) {
5003
- for (const k in qtiVar.base) {
5004
- if (!Object.prototype.hasOwnProperty.call(qtiVar.base, k)) continue;
5005
- const v = qtiVar.base[k];
5006
- if (v !== null && v !== undefined && v !== '') return true;
4614
+ #pci-container {
4615
+ width: 100%;
5007
4616
  }
5008
- }
5009
- if (qtiVar.list) {
5010
- for (const k in qtiVar.list) {
5011
- if (!Object.prototype.hasOwnProperty.call(qtiVar.list, k)) continue;
5012
- const v = qtiVar.list[k];
5013
- if (Array.isArray(v) && v.some(x => x !== null && x !== undefined && x !== '')) return true;
4617
+ qti-interaction-markup {
4618
+ display: block;
4619
+ width: 100%;
4620
+ min-height: 50px;
5014
4621
  }
5015
- }
5016
- if (Array.isArray(qtiVar.record) && qtiVar.record.length > 0) return true;
5017
- return false;
5018
- }
4622
+ </style>
4623
+ <link href="https://unpkg.com/@citolab/qti-components@latest/dist/item.css" rel="stylesheet" />
4624
+ <script src="${this.requireJsUrl}"></script>
4625
+ <script>
4626
+ const forwardConsole = ${forwardConsole ? "true" : "false"};
4627
+ if (forwardConsole) {
4628
+ const originalLog = console.log.bind(console);
4629
+ const originalError = console.error.bind(console);
4630
+ const stringifyArgs = args =>
4631
+ args.map(arg => {
4632
+ if (typeof arg === 'string') return arg;
4633
+ try {
4634
+ return JSON.stringify(arg);
4635
+ } catch (e) {
4636
+ return String(arg);
4637
+ }
4638
+ });
4639
+ console.log = (...args) => {
4640
+ originalLog(...args);
4641
+ window.parent.postMessage(
4642
+ {
4643
+ source: 'qti-pci-iframe',
4644
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4645
+ method: 'console',
4646
+ level: 'log',
4647
+ args: stringifyArgs(args)
4648
+ },
4649
+ '*'
4650
+ );
4651
+ };
4652
+ console.error = (...args) => {
4653
+ originalError(...args);
4654
+ window.parent.postMessage(
4655
+ {
4656
+ source: 'qti-pci-iframe',
4657
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4658
+ method: 'console',
4659
+ level: 'error',
4660
+ args: stringifyArgs(args)
4661
+ },
4662
+ '*'
4663
+ );
4664
+ };
4665
+ }
4666
+ // Define standard paths and shims
4667
+ window.requirePaths = ${requirePaths};
4668
+
4669
+ window.requireShim = ${requireShim};
4670
+
4671
+ // Single initial RequireJS configuration with error handling
4672
+ window.requirejs.config({
4673
+ catchError: true,
4674
+ waitSeconds: 30,
4675
+ paths: window.requirePaths,
4676
+ baseUrl: '${iframeBaseUrl}',
4677
+ shim: window.requireShim,
4678
+ onNodeCreated: function (node, config, moduleName, url) {
4679
+ // Add error handler to script node
4680
+ node.addEventListener('error', function (evt) {
4681
+ console.error('Script load error for module:', moduleName, 'URL:', url, 'Event:', evt);
4682
+ });
4683
+ },
4684
+ onError: function (err) {
4685
+ console.error('RequireJS error:', {
4686
+ type: err.requireType,
4687
+ modules: err.requireModules,
4688
+ error: err
4689
+ });
4690
+
4691
+ if (err.requireType === 'scripterror') {
4692
+ console.error('Script error usually indicates a network or CORS issue with:', err.requireModules);
4693
+ }
5019
4694
 
5020
- const initialBoundTo = config.boundTo && config.boundTo[this.responseIdentifier];
5021
- this.hadResponse = qtiVariableHasValue(initialBoundTo);
5022
- this.lastResponseStr = this.hadResponse ? JSON.stringify(initialBoundTo) : null;
5023
- // Ensure expected DOM structure exists (markup + properties)
5024
- this.markupEl = this.container.querySelector('qti-interaction-markup');
5025
- if (!this.markupEl) {
5026
- this.markupEl = document.createElement('qti-interaction-markup');
5027
- this.container.appendChild(this.markupEl);
5028
- }
5029
- this.markupEl.classList.add('qti-customInteraction');
5030
- this.propertiesEl = this.container.querySelector('properties');
5031
- if (!this.propertiesEl) {
5032
- this.propertiesEl = document.createElement('properties');
5033
- this.propertiesEl.style.display = 'none';
5034
- this.container.appendChild(this.propertiesEl);
5035
- } else {
5036
- this.propertiesEl.style.display = 'none';
5037
- }
4695
+ // Notify parent window about the error
4696
+ window.parent.postMessage(
4697
+ {
4698
+ source: 'qti-pci-iframe',
4699
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
4700
+ method: 'error',
4701
+ params: {
4702
+ message: 'RequireJS ' + err.requireType + ' error for modules: ' + err.requireModules,
4703
+ details: {
4704
+ type: err.requireType,
4705
+ modules: err.requireModules,
4706
+ error: err.toString()
4707
+ }
4708
+ }
4709
+ },
4710
+ '*'
4711
+ );
4712
+ }
4713
+ });
5038
4714
 
5039
- // Apply any markup/properties that arrived before initialization
5040
- if (this.pendingMarkup !== null) {
5041
- this.setMarkup(this.pendingMarkup);
5042
- this.pendingMarkup = null;
5043
- }
5044
- if (this.pendingProperties !== null) {
5045
- this.setProperties(this.pendingProperties);
5046
- this.pendingProperties = null;
5047
- }
5048
- if (this.pendingStylesheets !== null) {
5049
- this.setStylesheets(this.pendingStylesheets);
5050
- this.pendingStylesheets = null;
5051
- }
4715
+ // PCI Manager for iframe implementation
4716
+ window.PCIManager = {
4717
+ pciInstance: null,
4718
+ container: null,
4719
+ markupEl: null,
4720
+ propertiesEl: null,
4721
+ customInteractionTypeIdentifier: null,
4722
+ responseIdentifier: null,
4723
+ pendingBoundTo: null,
4724
+ pendingMarkup: null,
4725
+ pendingProperties: null,
4726
+ pendingState: null,
4727
+ pendingStylesheets: null,
4728
+ stylesheetKeys: {},
4729
+ interactionChangedViaEvent: false,
4730
+ eventBridgeAttached: false,
4731
+ lastResponseStr: null,
4732
+ hadResponse: false,
4733
+
4734
+ initialize: function (config) {
4735
+ this.customInteractionTypeIdentifier = config.customInteractionTypeIdentifier;
4736
+ this.responseIdentifier = config.responseIdentifier;
4737
+ this.container = document.getElementById('pci-container');
4738
+ this.container.classList.add('qti-customInteraction');
4739
+
4740
+ function qtiVariableHasValue(qtiVar) {
4741
+ if (!qtiVar) return false;
4742
+ if (qtiVar.base) {
4743
+ for (const k in qtiVar.base) {
4744
+ if (!Object.prototype.hasOwnProperty.call(qtiVar.base, k)) continue;
4745
+ const v = qtiVar.base[k];
4746
+ if (v !== null && v !== undefined && v !== '') return true;
4747
+ }
4748
+ }
4749
+ if (qtiVar.list) {
4750
+ for (const k in qtiVar.list) {
4751
+ if (!Object.prototype.hasOwnProperty.call(qtiVar.list, k)) continue;
4752
+ const v = qtiVar.list[k];
4753
+ if (Array.isArray(v) && v.some(x => x !== null && x !== undefined && x !== '')) return true;
4754
+ }
4755
+ }
4756
+ if (Array.isArray(qtiVar.record) && qtiVar.record.length > 0) return true;
4757
+ return false;
4758
+ }
5052
4759
 
5053
- // Bridge qti-interaction-changed events (preferred over polling)
5054
- if (!this.eventBridgeAttached) {
5055
- this.eventBridgeAttached = true;
5056
- const self = this;
5057
- this.container.addEventListener(
5058
- 'qti-interaction-changed',
5059
- function(evt) {
5060
- try {
5061
- self.interactionChangedViaEvent = true;
5062
- const value = evt && evt.detail ? evt.detail.value : undefined;
5063
- if (value !== undefined) {
5064
- const state = self.pciInstance && typeof self.pciInstance.getState === 'function' ? self.pciInstance.getState() : null;
5065
- self.notifyInteractionChanged(value, state);
5066
- }
5067
- } catch (e) {
5068
- // ignore bridge errors, polling fallback may still work
5069
- }
5070
- },
5071
- true
5072
- );
5073
- }
4760
+ const initialBoundTo = config.boundTo && config.boundTo[this.responseIdentifier];
4761
+ this.hadResponse = qtiVariableHasValue(initialBoundTo);
4762
+ this.lastResponseStr = this.hadResponse ? JSON.stringify(initialBoundTo) : null;
4763
+ // Ensure expected DOM structure exists (markup + properties)
4764
+ this.markupEl = this.container.querySelector('qti-interaction-markup');
4765
+ if (!this.markupEl) {
4766
+ this.markupEl = document.createElement('qti-interaction-markup');
4767
+ this.container.appendChild(this.markupEl);
4768
+ }
4769
+ this.markupEl.classList.add('qti-customInteraction');
4770
+ this.propertiesEl = this.container.querySelector('properties');
4771
+ if (!this.propertiesEl) {
4772
+ this.propertiesEl = document.createElement('properties');
4773
+ this.propertiesEl.style.display = 'none';
4774
+ this.container.appendChild(this.propertiesEl);
4775
+ } else {
4776
+ this.propertiesEl.style.display = 'none';
4777
+ }
5074
4778
 
5075
- function getResolvablePath(path, basePath) {
5076
- if (Array.isArray(path)) {
5077
- return path.map(p => getResolvablePathString(p, basePath));
5078
- } else {
5079
- return getResolvablePathString(path, basePath);
5080
- }
5081
- }
4779
+ // Apply any markup/properties that arrived before initialization
4780
+ if (this.pendingMarkup !== null) {
4781
+ this.setMarkup(this.pendingMarkup);
4782
+ this.pendingMarkup = null;
4783
+ }
4784
+ if (this.pendingProperties !== null) {
4785
+ this.setProperties(this.pendingProperties);
4786
+ this.pendingProperties = null;
4787
+ }
4788
+ if (this.pendingStylesheets !== null) {
4789
+ this.setStylesheets(this.pendingStylesheets);
4790
+ this.pendingStylesheets = null;
4791
+ }
5082
4792
 
5083
- function removeDoubleSlashes(str) {
5084
- return str
5085
- .replace(/([^:\\/])\\/\\/+/g, '$1/')
5086
- .replace(/\\/\\//g, '/')
5087
- .replace('http:/', 'http://')
5088
- .replace('https:/', 'https://');
5089
- }
4793
+ // Bridge qti-interaction-changed events (preferred over polling)
4794
+ if (!this.eventBridgeAttached) {
4795
+ this.eventBridgeAttached = true;
4796
+ const self = this;
4797
+ this.container.addEventListener(
4798
+ 'qti-interaction-changed',
4799
+ function (evt) {
4800
+ try {
4801
+ self.interactionChangedViaEvent = true;
4802
+ const value = evt && evt.detail ? evt.detail.value : undefined;
4803
+ if (value !== undefined) {
4804
+ const state =
4805
+ self.pciInstance && typeof self.pciInstance.getState === 'function'
4806
+ ? self.pciInstance.getState()
4807
+ : null;
4808
+ self.notifyInteractionChanged(value, state);
4809
+ }
4810
+ } catch (e) {
4811
+ // ignore bridge errors, polling fallback may still work
4812
+ }
4813
+ },
4814
+ true
4815
+ );
4816
+ }
5090
4817
 
5091
- function getResolvablePathString(path, basePath) {
5092
- path = path.replace(/\\.js$/, '');
5093
- return path?.toLocaleLowerCase().startsWith('http') || !basePath
5094
- ? path
5095
- : removeDoubleSlashes(\`\${basePath}/\${path}\`);
5096
- }
4818
+ function getResolvablePath(path, basePath) {
4819
+ if (Array.isArray(path)) {
4820
+ return path.map(p => getResolvablePathString(p, basePath));
4821
+ } else {
4822
+ return getResolvablePathString(path, basePath);
4823
+ }
4824
+ }
5097
4825
 
5098
- function combineRequireResolvePaths(path1, path2, baseUrl) {
5099
- path1 = getResolvablePath(path1, baseUrl);
5100
- const path1Array = Array.isArray(path1) ? path1 : [path1];
5101
- if (!path2) {
5102
- return path1Array;
5103
- }
5104
- path2 = getResolvablePath(path2, baseUrl);
5105
- const path2Array = Array.isArray(path2) ? path2 : [path2];
5106
- return path1Array.concat(path2Array).filter((value, index, self) => self.indexOf(value) === index);
5107
- }
4826
+ function removeDoubleSlashes(str) {
4827
+ return str
4828
+ .replace(/([^:\\/])\\/\\/+/g, '$1/')
4829
+ .replace(/\\/\\//g, '/')
4830
+ .replace('http:/', 'http://')
4831
+ .replace('https:/', 'https://');
4832
+ }
5108
4833
 
5109
- // Update paths with modules from the config
5110
- if (config.interactionModules && config.interactionModules.length > 0) {
5111
- config.interactionModules.forEach(module => {
5112
- if (module.id && module.primaryPath) {
5113
- const currentPath = window.requirePaths[module.id] || [];
5114
- const currentPaths = Array.isArray(currentPath) ? currentPath : [currentPath];
5115
- const newPath = combineRequireResolvePaths(
5116
- module.primaryPath, module.fallbackPath, config.baseUrl
5117
- );
5118
- window.requirePaths[module.id] = currentPaths.concat(newPath).filter((value, index, self) => self.indexOf(value) === index);
5119
- }
5120
- });
5121
- }
5122
4834
 
5123
- // The ONLY other requirejs.config call - with the context for this specific PCI
5124
- window.requirejs.config({
5125
- context: this.customInteractionTypeIdentifier,
5126
- paths: window.requirePaths,
5127
- shim: window.requireShim
5128
- });
5129
4835
 
5130
- // Define qtiCustomInteractionContext for the PCI
5131
- define('qtiCustomInteractionContext', () => {
5132
- return {
5133
- register: pciInstance => {
5134
- this.pciInstance = pciInstance;
5135
- // Configure PCI instance
5136
- const pciConfig = {
5137
- properties: config.properties || {},
5138
- contextVariables: config.contextVariables || {},
5139
- templateVariables: config.templateVariables || {},
5140
- onready: pciInstance => {
5141
- this.pciInstance = pciInstance;
5142
- // Apply any pending updates that arrived before onready
5143
- if (this.pendingBoundTo) {
5144
- this.applyBoundTo(this.pendingBoundTo);
5145
- this.pendingBoundTo = null;
4836
+ function combineRequireResolvePaths(path1, path2, baseUrl) {
4837
+ path1 = getResolvablePath(path1, baseUrl);
4838
+ const path1Array = Array.isArray(path1) ? path1 : [path1];
4839
+ if (!path2) {
4840
+ return path1Array;
5146
4841
  }
5147
- if (this.pendingState && typeof this.pciInstance.setState === 'function') {
5148
- this.pciInstance.setState(this.pendingState);
5149
- this.pendingState = null;
5150
- }
5151
- this.notifyReady();
5152
- },
5153
- ondone: (pciInstance, response, state, status) => {
5154
- this.notifyInteractionChanged(response, typeof state === 'string' ? state : null);
5155
- },
5156
- responseIdentifier: config.responseIdentifier,
5157
- boundTo: config.boundTo,
5158
- };
5159
-
5160
- if (pciInstance.getInstance) {
5161
- const dom = this.markupEl || this.container;
5162
- // Round-trip support for object states (stored as a prefixed JSON string by the host).
5163
- // For strict string-based PCIs we pass the original string through unchanged.
5164
- let restoredState = config.state;
5165
- if (typeof restoredState === 'string' && restoredState.indexOf('__qti_json__::') === 0) {
5166
- try {
5167
- restoredState = JSON.parse(restoredState.substring('__qti_json__::'.length));
5168
- } catch (e) {
5169
- // If parsing fails, fall back to the raw string.
5170
- restoredState = config.state;
5171
- }
5172
- }
5173
- pciInstance.getInstance(dom, pciConfig, restoredState || undefined);
5174
- } else {
5175
- // TAO custom interaction initialization
5176
- const restoreTAOConfig = (dataset) => {
5177
- const config = {};
5178
- const parseDataAttributes = () => {
5179
- const result = {};
5180
-
5181
- // Separate direct attributes from nested ones
5182
- Object.entries(dataset || {}).forEach(([key, value]) => {
5183
- if (!key.includes('__')) {
5184
- // Direct attributes (like version)
5185
- result[key] = value;
5186
- }
5187
- });
5188
-
5189
- // Parse nested attributes
5190
- const nestedData = {};
5191
-
5192
- Object.entries(dataset || {}).forEach(([key, value]) => {
5193
- const parts = key.split('__');
5194
- if (parts.length > 1) {
5195
- const [group, index, prop] = parts;
5196
- nestedData[group] = nestedData[group] || {};
5197
- nestedData[group][index] = nestedData[group][index] || {};
5198
- nestedData[group][index][prop] = value;
4842
+ path2 = getResolvablePath(path2, baseUrl);
4843
+ const path2Array = Array.isArray(path2) ? path2 : [path2];
4844
+ return path1Array.concat(path2Array).filter((value, index, self) => self.indexOf(value) === index);
4845
+ }
4846
+
4847
+ // Update paths with modules from the config
4848
+ if (config.interactionModules && config.interactionModules.length > 0) {
4849
+ config.interactionModules.forEach(module => {
4850
+ if (module.id && module.primaryPath) {
4851
+ const currentPath = window.requirePaths[module.id] || [];
4852
+ const currentPaths = Array.isArray(currentPath) ? currentPath : [currentPath];
4853
+ const newPath = combineRequireResolvePaths(
4854
+ module.primaryPath,
4855
+ module.fallbackPath,
4856
+ config.baseUrl
4857
+ );
4858
+ window.requirePaths[module.id] = currentPaths
4859
+ .concat(newPath)
4860
+ .filter((value, index, self) => self.indexOf(value) === index);
4861
+ }
4862
+ });
4863
+ }
4864
+
4865
+ // The ONLY other requirejs.config call - with the context for this specific PCI
4866
+ window.requirejs.config({
4867
+ context: this.customInteractionTypeIdentifier,
4868
+ paths: window.requirePaths,
4869
+ shim: window.requireShim
4870
+ });
4871
+
4872
+ // Define qtiCustomInteractionContext for the PCI
4873
+ define('qtiCustomInteractionContext', () => {
4874
+ return {
4875
+ register: pciInstance => {
4876
+ this.pciInstance = pciInstance;
4877
+ // Configure PCI instance
4878
+ const pciConfig = {
4879
+ properties: config.properties || {},
4880
+ contextVariables: config.contextVariables || {},
4881
+ templateVariables: config.templateVariables || {},
4882
+ onready: pciInstance => {
4883
+ this.pciInstance = pciInstance;
4884
+ // Apply any pending updates that arrived before onready
4885
+ if (this.pendingBoundTo) {
4886
+ this.applyBoundTo(this.pendingBoundTo);
4887
+ this.pendingBoundTo = null;
4888
+ }
4889
+ if (this.pendingState && typeof this.pciInstance.setState === 'function') {
4890
+ this.pciInstance.setState(this.pendingState);
4891
+ this.pendingState = null;
4892
+ }
4893
+ this.notifyReady();
4894
+ },
4895
+ ondone: (pciInstance, response, state, status) => {
4896
+ this.notifyInteractionChanged(response, typeof state === 'string' ? state : null);
4897
+ },
4898
+ responseIdentifier: config.responseIdentifier,
4899
+ boundTo: config.boundTo
4900
+ };
4901
+
4902
+ if (pciInstance.getInstance) {
4903
+ const dom = this.markupEl || this.container;
4904
+ // Round-trip support for object states (stored as a prefixed JSON string by the host).
4905
+ // For strict string-based PCIs we pass the original string through unchanged.
4906
+ let restoredState = config.state;
4907
+ if (typeof restoredState === 'string' && restoredState.indexOf('__qti_json__::') === 0) {
4908
+ try {
4909
+ restoredState = JSON.parse(restoredState.substring('__qti_json__::'.length));
4910
+ } catch (e) {
4911
+ // If parsing fails, fall back to the raw string.
4912
+ restoredState = config.state;
4913
+ }
5199
4914
  }
5200
- });
5201
-
5202
- // Convert nested groups to arrays
5203
- Object.entries(nestedData).forEach(([key, group]) => {
5204
- result[key] = Object.values(group);
5205
- });
5206
- return result;
5207
- };
5208
- const data = parseDataAttributes();
5209
- for (const key in data) {
5210
- if (Object.prototype.hasOwnProperty.call(data, key)) {
5211
- const value = data[key];
5212
- if (key === 'config') {
5213
- config[key] = JSON.parse(value);
5214
- } else {
5215
- config[key] = value;
4915
+ pciInstance.getInstance(dom, pciConfig, restoredState || undefined);
4916
+ } else {
4917
+ // TAO custom interaction initialization
4918
+ const restoreTAOConfig = dataset => {
4919
+ const config = {};
4920
+ const parseDataAttributes = () => {
4921
+ const result = {};
4922
+
4923
+ // Separate direct attributes from nested ones
4924
+ Object.entries(dataset || {}).forEach(([key, value]) => {
4925
+ if (!key.includes('__')) {
4926
+ // Direct attributes (like version)
4927
+ result[key] = value;
4928
+ }
4929
+ });
4930
+
4931
+ // Parse nested attributes
4932
+ const nestedData = {};
4933
+
4934
+ Object.entries(dataset || {}).forEach(([key, value]) => {
4935
+ const parts = key.split('__');
4936
+ if (parts.length > 1) {
4937
+ const [group, index, prop] = parts;
4938
+ nestedData[group] = nestedData[group] || {};
4939
+ nestedData[group][index] = nestedData[group][index] || {};
4940
+ nestedData[group][index][prop] = value;
4941
+ }
4942
+ });
4943
+
4944
+ // Convert nested groups to arrays
4945
+ Object.entries(nestedData).forEach(([key, group]) => {
4946
+ result[key] = Object.values(group);
4947
+ });
4948
+ return result;
4949
+ };
4950
+ const data = parseDataAttributes();
4951
+ for (const key in data) {
4952
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
4953
+ const value = data[key];
4954
+ if (key === 'config') {
4955
+ config[key] = JSON.parse(value);
4956
+ } else {
4957
+ config[key] = value;
4958
+ }
4959
+ }
4960
+ }
4961
+ return config;
4962
+ };
4963
+ const taoConfig = restoreTAOConfig(config.dataAttributes);
4964
+
4965
+ this.pciInstance.initialize(
4966
+ this.customInteractionTypeIdentifier,
4967
+ (this.markupEl || this.container).firstElementChild || this.markupEl || this.container,
4968
+ Object.keys(taoConfig).length ? taoConfig : null
4969
+ );
4970
+ }
4971
+ },
4972
+ notifyReady: () => {
4973
+ PCIManager.notifyReady();
5216
4974
  }
5217
- }
4975
+ };
4976
+ });
4977
+
4978
+ function getResolvablePathString(path, basePath) {
4979
+ path = path.replace(/\\.js$/, '');
4980
+ return path?.toLocaleLowerCase().startsWith('http') || !basePath
4981
+ ? path
4982
+ : removeDoubleSlashes(\`\${basePath}/\${path}\`);
5218
4983
  }
5219
- return config;
5220
- };
5221
- const taoConfig = restoreTAOConfig(config.dataAttributes);
5222
4984
 
5223
- this.pciInstance.initialize(
5224
- this.customInteractionTypeIdentifier,
5225
- (this.markupEl || this.container).firstElementChild || (this.markupEl || this.container),
5226
- Object.keys(taoConfig).length ? taoConfig : null
5227
- );
5228
- }
5229
- },
5230
- notifyReady: () => {
5231
- PCIManager.notifyReady();
5232
- }
5233
- };
5234
- });
4985
+ // Load the PCI module
4986
+ this.loadModule(config.module);
4987
+ },
5235
4988
 
5236
- // Load the PCI module
5237
- this.loadModule(config.module);
5238
- },
4989
+ loadModule: function (modulePath) {
4990
+ try {
4991
+ // Get the context-specific require
4992
+ const contextRequire = window.requirejs.config({
4993
+ context: this.customInteractionTypeIdentifier
4994
+ });
4995
+ contextRequire(['require'], require => {
4996
+ // Now load the actual module
4997
+ require([modulePath], () => {}, err => {
4998
+ console.error('Error loading module:', modulePath, err);
4999
+ this.notifyError('Module load error: ' + err.toString());
5000
+ });
5001
+ });
5002
+ } catch (error) {
5003
+ console.error('Exception in loadModule:', modulePath);
5004
+ console.error(error);
5005
+ this.notifyError('Error in require call: ' + error.toString());
5006
+ }
5007
+ },
5239
5008
 
5240
- loadModule: function(modulePath) {
5241
- try {
5242
- // Get the context-specific require
5243
- const contextRequire = window.requirejs.config({
5244
- context: this.customInteractionTypeIdentifier
5245
- });
5246
- contextRequire(['require'], require => {
5247
- // Now load the actual module
5248
- require([modulePath], () => {
5249
- }, err => {
5250
- console.error('Error loading module:', modulePath, err);
5251
- this.notifyError('Module load error: ' + err.toString());
5252
- });
5253
- });
5254
- } catch (error) {
5255
- console.error('Exception in loadModule:', modulePath);
5256
- console.error(error);
5257
- this.notifyError('Error in require call: ' + error.toString());
5258
- }
5259
- },
5009
+ notifyReady: function () {
5010
+ window.parent.postMessage(
5011
+ {
5012
+ source: 'qti-pci-iframe',
5013
+ responseIdentifier: this.responseIdentifier,
5014
+ method: 'iframeReady'
5015
+ },
5016
+ '*'
5017
+ );
5018
+ },
5260
5019
 
5261
- notifyReady: function() {
5262
- window.parent.postMessage({
5263
- source: 'qti-pci-iframe',
5264
- responseIdentifier: this.responseIdentifier,
5265
- method: 'iframeReady'
5266
- }, '*');
5267
- },
5268
-
5269
- notifyInteractionChanged: function(response, state) {
5270
- window.parent.postMessage({
5271
- source: 'qti-pci-iframe',
5272
- responseIdentifier: this.responseIdentifier,
5273
- method: 'interactionChanged',
5274
- params: { value: response, state: state }
5275
- }, '*');
5276
- },
5277
-
5278
- notifyError: function(message) {
5279
- console.error('PCI Error:', message);
5280
- window.parent.postMessage({
5281
- source: 'qti-pci-iframe',
5282
- responseIdentifier: this.responseIdentifier,
5283
- method: 'error',
5284
- params: { message: message }
5285
- }, '*');
5286
- },
5287
-
5288
- setMarkup: function(markupHtml) {
5289
- if (!this.container) {
5290
- this.container = document.getElementById('pci-container');
5291
- }
5292
- if (!this.container) {
5293
- this.pendingMarkup = markupHtml;
5294
- return;
5295
- }
5296
- this.markupEl = this.container.querySelector('qti-interaction-markup');
5297
- if (!this.markupEl) {
5298
- this.markupEl = document.createElement('qti-interaction-markup');
5299
- this.container.appendChild(this.markupEl);
5300
- }
5301
- this.markupEl.classList.add('qti-customInteraction');
5302
- this.markupEl.innerHTML = markupHtml || '';
5303
- },
5020
+ notifyInteractionChanged: function (response, state) {
5021
+ window.parent.postMessage(
5022
+ {
5023
+ source: 'qti-pci-iframe',
5024
+ responseIdentifier: this.responseIdentifier,
5025
+ method: 'interactionChanged',
5026
+ params: { value: response, state: state }
5027
+ },
5028
+ '*'
5029
+ );
5030
+ },
5304
5031
 
5305
- setProperties: function(propertiesHtml) {
5306
- if (!this.container) {
5307
- this.container = document.getElementById('pci-container');
5308
- }
5309
- if (!this.container) {
5310
- this.pendingProperties = propertiesHtml;
5311
- return;
5312
- }
5313
- this.propertiesEl = this.container.querySelector('properties');
5314
- if (!this.propertiesEl) {
5315
- this.propertiesEl = document.createElement('properties');
5316
- this.container.appendChild(this.propertiesEl);
5317
- }
5318
- this.propertiesEl.style.display = 'none';
5319
- this.propertiesEl.innerHTML = propertiesHtml || '';
5320
- },
5032
+ notifyError: function (message) {
5033
+ console.error('PCI Error:', message);
5034
+ window.parent.postMessage(
5035
+ {
5036
+ source: 'qti-pci-iframe',
5037
+ responseIdentifier: this.responseIdentifier,
5038
+ method: 'error',
5039
+ params: { message: message }
5040
+ },
5041
+ '*'
5042
+ );
5043
+ },
5321
5044
 
5322
- minifyCss: function(cssContent) {
5323
- return cssContent
5324
- .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')
5325
- .replace(/\\s+/g, ' ')
5326
- .replace(/\\s*([{}:;])\\s*/g, '$1')
5327
- .trim();
5328
- },
5045
+ setMarkup: function (markupHtml) {
5046
+ if (!this.container) {
5047
+ this.container = document.getElementById('pci-container');
5048
+ }
5049
+ if (!this.container) {
5050
+ this.pendingMarkup = markupHtml;
5051
+ return;
5052
+ }
5053
+ this.markupEl = this.container.querySelector('qti-interaction-markup');
5054
+ if (!this.markupEl) {
5055
+ this.markupEl = document.createElement('qti-interaction-markup');
5056
+ this.container.appendChild(this.markupEl);
5057
+ }
5058
+ this.markupEl.classList.add('qti-customInteraction');
5059
+ this.markupEl.innerHTML = markupHtml || '';
5060
+ },
5329
5061
 
5330
- injectStylesheet: function(cssContent, key, scoped) {
5331
- if (!cssContent) return;
5332
- const head = document.head || document.getElementsByTagName('head')[0] || document.body;
5333
- if (!head) return;
5334
- const resolvedKey = key || '';
5335
- if (resolvedKey && this.stylesheetKeys[resolvedKey]) return;
5336
- const shouldScope = scoped !== false;
5337
- const styleEl = document.createElement('style');
5338
- styleEl.media = 'screen';
5339
- if (resolvedKey) styleEl.setAttribute('data-qti-stylesheet', resolvedKey);
5340
- const minified = this.minifyCss(cssContent);
5341
- styleEl.textContent = shouldScope ? '@scope {' + minified + '}' : minified;
5342
- head.appendChild(styleEl);
5343
- if (resolvedKey) this.stylesheetKeys[resolvedKey] = true;
5344
- },
5062
+ setProperties: function (propertiesHtml) {
5063
+ if (!this.container) {
5064
+ this.container = document.getElementById('pci-container');
5065
+ }
5066
+ if (!this.container) {
5067
+ this.pendingProperties = propertiesHtml;
5068
+ return;
5069
+ }
5070
+ this.propertiesEl = this.container.querySelector('properties');
5071
+ if (!this.propertiesEl) {
5072
+ this.propertiesEl = document.createElement('properties');
5073
+ this.container.appendChild(this.propertiesEl);
5074
+ }
5075
+ this.propertiesEl.style.display = 'none';
5076
+ this.propertiesEl.innerHTML = propertiesHtml || '';
5077
+ },
5345
5078
 
5346
- setStylesheets: function(stylesheets) {
5347
- if (!Array.isArray(stylesheets)) return;
5348
- stylesheets.forEach((sheet, index) => {
5349
- if (!sheet) return;
5350
- const key = sheet.key || sheet.href || ('inline-' + index);
5351
- const scoped = sheet.scoped !== false;
5352
- if (sheet.content) {
5353
- this.injectStylesheet(sheet.content, key, scoped);
5354
- return;
5355
- }
5356
- if (sheet.href) {
5357
- fetch(sheet.href)
5358
- .then(resp => resp.text())
5359
- .then(css => this.injectStylesheet(css, key, scoped))
5360
- .catch(() => {
5361
- // ignore stylesheet load errors
5362
- });
5363
- }
5364
- });
5365
- },
5079
+ minifyCss: function (cssContent) {
5080
+ return cssContent
5081
+ .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')
5082
+ .replace(/\\s+/g, ' ')
5083
+ .replace(/\\s*([{}:;])\\s*/g, '$1')
5084
+ .trim();
5085
+ },
5366
5086
 
5367
- applyBoundTo: function(boundTo) {
5368
- if (!this.pciInstance || typeof this.pciInstance.setResponse !== 'function') return;
5369
- const value = boundTo && (boundTo[this.responseIdentifier] || boundTo[Object.keys(boundTo)[0]]);
5370
- if (value) this.pciInstance.setResponse(value);
5371
- },
5372
- };
5087
+ setStylesheets: function (stylesheetsHtml) {
5088
+ if (!stylesheetsHtml) return;
5089
+ if (!this.container) {
5090
+ this.container = document.getElementById('pci-container');
5091
+ }
5092
+ const target = this.container || document.body;
5093
+ if (!target) return;
5094
+ target.insertAdjacentHTML('afterbegin', stylesheetsHtml);
5095
+ },
5373
5096
 
5374
- // Set up message listener for communication with parent
5375
- let expectedParentOrigin = null;
5376
- window.addEventListener('message', function(event) {
5377
- const { data } = event;
5378
-
5379
- // Ensure the message is from our parent
5380
- if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') {
5381
- return;
5382
- }
5383
- if (expectedParentOrigin === null) {
5384
- expectedParentOrigin = event.origin;
5385
- } else if (event.origin !== expectedParentOrigin) {
5386
- return;
5387
- }
5388
-
5389
- function deepQuerySelector(root, selector) {
5390
- if (!root) return null;
5391
- try {
5392
- const direct = root.querySelector ? root.querySelector(selector) : null;
5393
- if (direct) return direct;
5394
- } catch (e) {
5395
- // ignore invalid selector for this root
5396
- }
5397
- if (!root.querySelectorAll) return null;
5398
- const nodes = root.querySelectorAll('*');
5399
- for (const node of nodes) {
5400
- if (node && node.shadowRoot) {
5401
- const found = deepQuerySelector(node.shadowRoot, selector);
5402
- if (found) return found;
5403
- }
5404
- }
5405
- return null;
5406
- }
5407
5097
 
5408
- function deepFindElementByExactText(root, text) {
5409
- if (!root || !text) return null;
5410
- if (root.querySelectorAll) {
5411
- const nodes = root.querySelectorAll('*');
5412
- for (const node of nodes) {
5413
- if ((node.textContent || '').trim() === text) return node;
5414
- if (node.shadowRoot) {
5415
- const found = deepFindElementByExactText(node.shadowRoot, text);
5416
- if (found) return found;
5417
- }
5418
- }
5419
- }
5420
- return null;
5421
- }
5098
+ applyBoundTo: function (boundTo) {
5099
+ if (!this.pciInstance || typeof this.pciInstance.setResponse !== 'function') return;
5100
+ const value = boundTo && (boundTo[this.responseIdentifier] || boundTo[Object.keys(boundTo)[0]]);
5101
+ if (value) this.pciInstance.setResponse(value);
5102
+ }
5103
+ };
5422
5104
 
5423
- switch(data.method) {
5424
- case 'initialize':
5425
- PCIManager.initialize(data.params);
5426
- break;
5105
+ // Set up message listener for communication with parent
5106
+ let expectedParentOrigin = null;
5107
+ window.addEventListener('message', function (event) {
5108
+ const { data } = event;
5427
5109
 
5428
- case 'setMarkup':
5429
- PCIManager.setMarkup(data.params);
5430
- break;
5110
+ // Ensure the message is from our parent
5111
+ if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') {
5112
+ return;
5113
+ }
5114
+ if (expectedParentOrigin === null) {
5115
+ expectedParentOrigin = event.origin;
5116
+ } else if (event.origin !== expectedParentOrigin) {
5117
+ return;
5118
+ }
5431
5119
 
5432
- case 'setBoundTo':
5433
- if (PCIManager.pciInstance) {
5434
- PCIManager.applyBoundTo(data.params);
5435
- } else {
5436
- PCIManager.pendingBoundTo = data.params;
5437
- }
5438
- break;
5120
+ function deepQuerySelector(root, selector) {
5121
+ if (!root) return null;
5122
+ try {
5123
+ const direct = root.querySelector ? root.querySelector(selector) : null;
5124
+ if (direct) return direct;
5125
+ } catch (e) {
5126
+ // ignore invalid selector for this root
5127
+ }
5128
+ if (!root.querySelectorAll) return null;
5129
+ const nodes = root.querySelectorAll('*');
5130
+ for (const node of nodes) {
5131
+ if (node && node.shadowRoot) {
5132
+ const found = deepQuerySelector(node.shadowRoot, selector);
5133
+ if (found) return found;
5134
+ }
5135
+ }
5136
+ return null;
5137
+ }
5439
5138
 
5440
- case 'setProperties':
5441
- PCIManager.setProperties(data.params);
5442
- break;
5139
+ function deepFindElementByExactText(root, text) {
5140
+ if (!root || !text) return null;
5141
+ if (root.querySelectorAll) {
5142
+ const nodes = root.querySelectorAll('*');
5143
+ for (const node of nodes) {
5144
+ if ((node.textContent || '').trim() === text) return node;
5145
+ if (node.shadowRoot) {
5146
+ const found = deepFindElementByExactText(node.shadowRoot, text);
5147
+ if (found) return found;
5148
+ }
5149
+ }
5150
+ }
5151
+ return null;
5152
+ }
5443
5153
 
5444
- case 'setStylesheets':
5445
- PCIManager.setStylesheets(data.params);
5446
- break;
5154
+ switch (data.method) {
5155
+ case 'initialize':
5156
+ PCIManager.initialize(data.params);
5157
+ break;
5447
5158
 
5448
- case 'setState':
5449
- if (PCIManager.pciInstance && typeof PCIManager.pciInstance.setState === 'function') {
5450
- PCIManager.pciInstance.setState((data.params && data.params.state) || data.params);
5451
- } else {
5452
- PCIManager.pendingState = (data.params && data.params.state) || data.params;
5453
- }
5454
- break;
5159
+ case 'setMarkup':
5160
+ PCIManager.setMarkup(data.params);
5161
+ break;
5455
5162
 
5456
- case 'getContent': {
5457
- const messageId = data.params && data.params.messageId;
5458
- const collectShadowHtml = root => {
5459
- const parts = [];
5460
- if (!root || !root.querySelectorAll) return parts;
5461
- const nodes = root.querySelectorAll('*');
5462
- for (const node of nodes) {
5463
- if (node && node.shadowRoot) {
5464
- parts.push(node.shadowRoot.innerHTML || '');
5465
- parts.push(...collectShadowHtml(node.shadowRoot));
5466
- }
5467
- }
5468
- return parts;
5469
- };
5470
- const shadowHtml = collectShadowHtml(document).join('\\n');
5471
- window.parent.postMessage(
5472
- {
5473
- source: 'qti-pci-iframe',
5474
- responseIdentifier: PCIManager.responseIdentifier,
5475
- method: 'getContentResponse',
5476
- messageId: messageId,
5477
- content: (document.documentElement ? document.documentElement.outerHTML : '') + '\\n' + shadowHtml
5478
- },
5479
- '*'
5480
- );
5481
- break;
5482
- }
5163
+ case 'setBoundTo':
5164
+ if (PCIManager.pciInstance) {
5165
+ PCIManager.applyBoundTo(data.params);
5166
+ } else {
5167
+ PCIManager.pendingBoundTo = data.params;
5168
+ }
5169
+ break;
5483
5170
 
5484
- case 'simulateClick': {
5485
- const messageId = data.params && data.params.messageId;
5486
- const x = data.params && data.params.x;
5487
- const y = data.params && data.params.y;
5488
- const el = typeof x === 'number' && typeof y === 'number' ? document.elementFromPoint(x, y) : null;
5489
- const target = (el && el.closest && el.closest('.hitbox')) || document.querySelector('.hitbox') || el;
5490
- if (target) {
5491
- const evt = new MouseEvent('click', {
5492
- bubbles: true,
5493
- clientX: x,
5494
- clientY: y,
5495
- screenX: x,
5496
- screenY: y,
5497
- view: window
5498
- });
5499
- target.dispatchEvent(evt);
5500
- }
5501
- window.parent.postMessage(
5502
- {
5503
- source: 'qti-pci-iframe',
5504
- responseIdentifier: PCIManager.responseIdentifier,
5505
- method: 'clickResponse',
5506
- messageId: messageId
5507
- },
5508
- '*'
5509
- );
5510
- break;
5511
- }
5512
-
5513
- case 'getBoundingRect': {
5514
- const messageId = data.params && data.params.messageId;
5515
- const selector = data.params && data.params.selector;
5516
- const el = selector ? deepQuerySelector(document, selector) : null;
5517
- const rect = el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
5518
- window.parent.postMessage(
5519
- {
5520
- source: 'qti-pci-iframe',
5521
- responseIdentifier: PCIManager.responseIdentifier,
5522
- method: 'getBoundingRectResponse',
5523
- messageId: messageId,
5524
- rect: rect
5525
- ? {
5526
- left: rect.left,
5527
- top: rect.top,
5528
- width: rect.width,
5529
- height: rect.height
5171
+ case 'setProperties':
5172
+ PCIManager.setProperties(data.params);
5173
+ break;
5174
+
5175
+ case 'setStylesheets':
5176
+ PCIManager.setStylesheets(data.params);
5177
+ break;
5178
+
5179
+ case 'setState':
5180
+ if (PCIManager.pciInstance && typeof PCIManager.pciInstance.setState === 'function') {
5181
+ PCIManager.pciInstance.setState((data.params && data.params.state) || data.params);
5182
+ } else {
5183
+ PCIManager.pendingState = (data.params && data.params.state) || data.params;
5184
+ }
5185
+ break;
5186
+
5187
+ case 'getContent': {
5188
+ const messageId = data.params && data.params.messageId;
5189
+ const collectShadowHtml = root => {
5190
+ const parts = [];
5191
+ if (!root || !root.querySelectorAll) return parts;
5192
+ const nodes = root.querySelectorAll('*');
5193
+ for (const node of nodes) {
5194
+ if (node && node.shadowRoot) {
5195
+ parts.push(node.shadowRoot.innerHTML || '');
5196
+ parts.push(...collectShadowHtml(node.shadowRoot));
5197
+ }
5530
5198
  }
5531
- : null
5532
- },
5533
- '*'
5534
- );
5535
- break;
5536
- }
5199
+ return parts;
5200
+ };
5201
+ const shadowHtml = collectShadowHtml(document).join('\\n');
5202
+ window.parent.postMessage(
5203
+ {
5204
+ source: 'qti-pci-iframe',
5205
+ responseIdentifier: PCIManager.responseIdentifier,
5206
+ method: 'getContentResponse',
5207
+ messageId: messageId,
5208
+ content: (document.documentElement ? document.documentElement.outerHTML : '') + '\\n' + shadowHtml
5209
+ },
5210
+ '*'
5211
+ );
5212
+ break;
5213
+ }
5537
5214
 
5538
- case 'clickOnSelector': {
5539
- const messageId = data.params && data.params.messageId;
5540
- const selector = data.params && data.params.selector;
5541
- const el = selector ? deepQuerySelector(document, selector) : null;
5542
- const success = !!el;
5543
- if (el && typeof el.click === 'function') el.click();
5544
- window.parent.postMessage(
5545
- {
5546
- source: 'qti-pci-iframe',
5547
- responseIdentifier: PCIManager.responseIdentifier,
5548
- method: 'clickSelectorResponse',
5549
- messageId: messageId,
5550
- success: success
5551
- },
5552
- '*'
5553
- );
5554
- break;
5555
- }
5556
-
5557
- case 'clickOnElementByText': {
5558
- const messageId = data.params && data.params.messageId;
5559
- const text = data.params && data.params.text;
5560
- const target = text ? deepFindElementByExactText(document, text) : null;
5561
- const success = !!target;
5562
- if (target && typeof target.click === 'function') target.click();
5563
- window.parent.postMessage(
5564
- {
5565
- source: 'qti-pci-iframe',
5566
- responseIdentifier: PCIManager.responseIdentifier,
5567
- method: 'clickTextResponse',
5568
- messageId: messageId,
5569
- success: success
5570
- },
5571
- '*'
5572
- );
5573
- break;
5574
- }
5575
-
5576
- case 'setValueElement': {
5577
- const messageId = data.params && data.params.messageId;
5578
- const selector = data.params && data.params.selector;
5579
- const value = data.params && data.params.value;
5580
- const el = selector ? deepQuerySelector(document, selector) : null;
5581
- let success = false;
5582
- if (el && 'value' in el) {
5583
- try {
5584
- el.value = value;
5585
- el.dispatchEvent(new Event('input', { bubbles: true }));
5586
- el.dispatchEvent(new Event('change', { bubbles: true }));
5587
- success = true;
5588
- } catch (e) {
5589
- success = false;
5590
- }
5591
- }
5592
- window.parent.postMessage(
5593
- {
5594
- source: 'qti-pci-iframe',
5595
- responseIdentifier: PCIManager.responseIdentifier,
5596
- method: 'setValueResponse',
5597
- messageId: messageId,
5598
- success: success
5599
- },
5600
- '*'
5601
- );
5602
- break;
5603
- }
5604
-
5605
- case 'console':
5606
- window.parent.postMessage(
5607
- {
5608
- source: 'qti-pci-iframe',
5609
- responseIdentifier: PCIManager.responseIdentifier,
5610
- method: 'console',
5611
- level: data.level,
5612
- args: data.args
5613
- },
5614
- '*'
5615
- );
5616
- break;
5617
- }
5618
- });
5215
+ case 'simulateClick': {
5216
+ const messageId = data.params && data.params.messageId;
5217
+ const x = data.params && data.params.x;
5218
+ const y = data.params && data.params.y;
5219
+ const el = typeof x === 'number' && typeof y === 'number' ? document.elementFromPoint(x, y) : null;
5220
+ const target = (el && el.closest && el.closest('.hitbox')) || document.querySelector('.hitbox') || el;
5221
+ if (target) {
5222
+ const evt = new MouseEvent('click', {
5223
+ bubbles: true,
5224
+ clientX: x,
5225
+ clientY: y,
5226
+ screenX: x,
5227
+ screenY: y,
5228
+ view: window
5229
+ });
5230
+ target.dispatchEvent(evt);
5231
+ }
5232
+ window.parent.postMessage(
5233
+ {
5234
+ source: 'qti-pci-iframe',
5235
+ responseIdentifier: PCIManager.responseIdentifier,
5236
+ method: 'clickResponse',
5237
+ messageId: messageId
5238
+ },
5239
+ '*'
5240
+ );
5241
+ break;
5242
+ }
5619
5243
 
5620
- let resizeTimeout;
5621
- let previousHeight = 0;
5622
- const notifyResize = () => {
5623
- const container = document.getElementById('pci-container');
5624
- const newHeight = container.scrollHeight + 100;
5625
- if (newHeight !== previousHeight) {
5626
- previousHeight = newHeight;
5627
- clearTimeout(resizeTimeout);
5628
- resizeTimeout = setTimeout(() => {
5629
- window.parent.postMessage({
5630
- source: 'qti-pci-iframe',
5631
- responseIdentifier: PCIManager.responseIdentifier,
5632
- method: 'resize',
5633
- height: newHeight,
5634
- width: container.scrollWidth
5635
- }, '*');
5636
- }, 100); // Adjust debounce time as needed
5637
- }
5638
- };
5244
+ case 'getBoundingRect': {
5245
+ const messageId = data.params && data.params.messageId;
5246
+ const selector = data.params && data.params.selector;
5247
+ const el = selector ? deepQuerySelector(document, selector) : null;
5248
+ const rect = el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
5249
+ window.parent.postMessage(
5250
+ {
5251
+ source: 'qti-pci-iframe',
5252
+ responseIdentifier: PCIManager.responseIdentifier,
5253
+ method: 'getBoundingRectResponse',
5254
+ messageId: messageId,
5255
+ rect: rect
5256
+ ? {
5257
+ left: rect.left,
5258
+ top: rect.top,
5259
+ width: rect.width,
5260
+ height: rect.height
5261
+ }
5262
+ : null
5263
+ },
5264
+ '*'
5265
+ );
5266
+ break;
5267
+ }
5639
5268
 
5640
- function setupResizeObserver() {
5641
- const container = document.getElementById('pci-container');
5642
- if (!container || !(container instanceof Element)) {
5643
- console.warn('ResizeObserver: document.container is not an Element');
5644
- return;
5645
- }
5269
+ case 'clickOnSelector': {
5270
+ const messageId = data.params && data.params.messageId;
5271
+ const selector = data.params && data.params.selector;
5272
+ const el = selector ? deepQuerySelector(document, selector) : null;
5273
+ const success = !!el;
5274
+ if (el && typeof el.click === 'function') el.click();
5275
+ window.parent.postMessage(
5276
+ {
5277
+ source: 'qti-pci-iframe',
5278
+ responseIdentifier: PCIManager.responseIdentifier,
5279
+ method: 'clickSelectorResponse',
5280
+ messageId: messageId,
5281
+ success: success
5282
+ },
5283
+ '*'
5284
+ );
5285
+ break;
5286
+ }
5646
5287
 
5647
- const resizeObserver = new ResizeObserver(() => {
5648
- notifyResize();
5649
- });
5288
+ case 'clickOnElementByText': {
5289
+ const messageId = data.params && data.params.messageId;
5290
+ const text = data.params && data.params.text;
5291
+ const target = text ? deepFindElementByExactText(document, text) : null;
5292
+ const success = !!target;
5293
+ if (target && typeof target.click === 'function') target.click();
5294
+ window.parent.postMessage(
5295
+ {
5296
+ source: 'qti-pci-iframe',
5297
+ responseIdentifier: PCIManager.responseIdentifier,
5298
+ method: 'clickTextResponse',
5299
+ messageId: messageId,
5300
+ success: success
5301
+ },
5302
+ '*'
5303
+ );
5304
+ break;
5305
+ }
5650
5306
 
5651
- resizeObserver.observe(container);
5652
- }
5307
+ case 'setValueElement': {
5308
+ const messageId = data.params && data.params.messageId;
5309
+ const selector = data.params && data.params.selector;
5310
+ const value = data.params && data.params.value;
5311
+ const el = selector ? deepQuerySelector(document, selector) : null;
5312
+ let success = false;
5313
+ if (el && 'value' in el) {
5314
+ try {
5315
+ el.value = value;
5316
+ el.dispatchEvent(new Event('input', { bubbles: true }));
5317
+ el.dispatchEvent(new Event('change', { bubbles: true }));
5318
+ success = true;
5319
+ } catch (e) {
5320
+ success = false;
5321
+ }
5322
+ }
5323
+ window.parent.postMessage(
5324
+ {
5325
+ source: 'qti-pci-iframe',
5326
+ responseIdentifier: PCIManager.responseIdentifier,
5327
+ method: 'setValueResponse',
5328
+ messageId: messageId,
5329
+ success: success
5330
+ },
5331
+ '*'
5332
+ );
5333
+ break;
5334
+ }
5653
5335
 
5654
- // Run setup once DOM is ready
5655
- if (document.readyState === 'loading') {
5656
- document.addEventListener('DOMContentLoaded', () => {
5657
- notifyResize(); // initial resize
5658
- setupResizeObserver();
5659
- });
5660
- } else {
5661
- notifyResize();
5662
- setupResizeObserver();
5663
- }
5336
+ case 'console':
5337
+ window.parent.postMessage(
5338
+ {
5339
+ source: 'qti-pci-iframe',
5340
+ responseIdentifier: PCIManager.responseIdentifier,
5341
+ method: 'console',
5342
+ level: data.level,
5343
+ args: data.args
5344
+ },
5345
+ '*'
5346
+ );
5347
+ break;
5348
+ }
5349
+ });
5664
5350
 
5665
- window.addEventListener('load', () => {
5666
- notifyResize();
5667
- });
5668
- let lastResponseStr = '';
5669
- setInterval(() => {
5670
- if (PCIManager.interactionChangedViaEvent) return;
5671
- if (PCIManager.pciInstance && PCIManager.pciInstance.getResponse) {
5672
- const response = PCIManager.pciInstance.getResponse();
5673
- if (response === undefined) {
5674
- // Don't emit an initial empty on load; only emit a clear if we previously had a value
5675
- if (!PCIManager.hadResponse) return;
5676
- PCIManager.hadResponse = false;
5677
- PCIManager.lastResponseStr = null;
5678
- const state = PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function' ? PCIManager.pciInstance.getState() : null;
5679
- window.parent.postMessage(
5680
- {
5681
- source: 'qti-pci-iframe',
5682
- responseIdentifier: PCIManager.responseIdentifier,
5683
- method: 'interactionChanged',
5684
- params: { value: null, state: state }
5685
- },
5686
- '*'
5687
- );
5688
- return;
5689
- }
5690
-
5691
- const responseStr = JSON.stringify(response);
5692
-
5693
- if (responseStr !== PCIManager.lastResponseStr) {
5694
- PCIManager.lastResponseStr = responseStr;
5695
- PCIManager.hadResponse = true;
5696
- const state = PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function' ? PCIManager.pciInstance.getState() : null;
5697
- window.parent.postMessage(
5698
- {
5699
- source: 'qti-pci-iframe',
5700
- responseIdentifier: PCIManager.responseIdentifier,
5701
- method: 'interactionChanged',
5702
- params: { value: response, state: state }
5703
- },
5704
- '*'
5705
- );
5706
- }
5707
- }
5708
- }, 500); // Check every 500ms
5709
- </script>
5710
- </head>
5711
- <body>
5712
- <div id="pci-container"></div>
5713
- </body>
5714
- </html>`;
5351
+ let resizeTimeout;
5352
+ let previousHeight = 0;
5353
+ const notifyResize = () => {
5354
+ const container = document.getElementById('pci-container');
5355
+ const newHeight = container.scrollHeight + 100;
5356
+ if (newHeight !== previousHeight) {
5357
+ previousHeight = newHeight;
5358
+ clearTimeout(resizeTimeout);
5359
+ resizeTimeout = setTimeout(() => {
5360
+ window.parent.postMessage(
5361
+ {
5362
+ source: 'qti-pci-iframe',
5363
+ responseIdentifier: PCIManager.responseIdentifier,
5364
+ method: 'resize',
5365
+ height: newHeight,
5366
+ width: container.scrollWidth
5367
+ },
5368
+ '*'
5369
+ );
5370
+ }, 100); // Adjust debounce time as needed
5371
+ }
5372
+ };
5373
+
5374
+ function setupResizeObserver() {
5375
+ const container = document.getElementById('pci-container');
5376
+ if (!container || !(container instanceof Element)) {
5377
+ console.warn('ResizeObserver: document.container is not an Element');
5378
+ return;
5379
+ }
5380
+
5381
+ const resizeObserver = new ResizeObserver(() => {
5382
+ notifyResize();
5383
+ });
5384
+
5385
+ resizeObserver.observe(container);
5386
+ }
5387
+
5388
+ // Run setup once DOM is ready
5389
+ if (document.readyState === 'loading') {
5390
+ document.addEventListener('DOMContentLoaded', () => {
5391
+ notifyResize(); // initial resize
5392
+ setupResizeObserver();
5393
+ });
5394
+ } else {
5395
+ notifyResize();
5396
+ setupResizeObserver();
5397
+ }
5398
+
5399
+ window.addEventListener('load', () => {
5400
+ notifyResize();
5401
+ });
5402
+ let lastResponseStr = '';
5403
+ setInterval(() => {
5404
+ if (PCIManager.interactionChangedViaEvent) return;
5405
+ if (PCIManager.pciInstance && PCIManager.pciInstance.getResponse) {
5406
+ const response = PCIManager.pciInstance.getResponse();
5407
+ if (response === undefined) {
5408
+ // Don't emit an initial empty on load; only emit a clear if we previously had a value
5409
+ if (!PCIManager.hadResponse) return;
5410
+ PCIManager.hadResponse = false;
5411
+ PCIManager.lastResponseStr = null;
5412
+ const state =
5413
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function'
5414
+ ? PCIManager.pciInstance.getState()
5415
+ : null;
5416
+ window.parent.postMessage(
5417
+ {
5418
+ source: 'qti-pci-iframe',
5419
+ responseIdentifier: PCIManager.responseIdentifier,
5420
+ method: 'interactionChanged',
5421
+ params: { value: null, state: state }
5422
+ },
5423
+ '*'
5424
+ );
5425
+ return;
5426
+ }
5427
+
5428
+ const responseStr = JSON.stringify(response);
5429
+
5430
+ if (responseStr !== PCIManager.lastResponseStr) {
5431
+ PCIManager.lastResponseStr = responseStr;
5432
+ PCIManager.hadResponse = true;
5433
+ const state =
5434
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function'
5435
+ ? PCIManager.pciInstance.getState()
5436
+ : null;
5437
+ window.parent.postMessage(
5438
+ {
5439
+ source: 'qti-pci-iframe',
5440
+ responseIdentifier: PCIManager.responseIdentifier,
5441
+ method: 'interactionChanged',
5442
+ params: { value: response, state: state }
5443
+ },
5444
+ '*'
5445
+ );
5446
+ }
5447
+ }
5448
+ }, 500); // Check every 500ms
5449
+ </script>
5450
+ </head>
5451
+ <body>
5452
+ <div id="pci-container"></div>
5453
+ </body>
5454
+ </html>`
5455
+ );
5715
5456
  }
5716
5457
  /**
5717
5458
  * Toggle the display of the correct response
@@ -7083,17 +6824,23 @@ var QtiHottext = class extends ActiveElementMixin(i2, "qti-hottext") {
7083
6824
  }
7084
6825
  };
7085
6826
 
6827
+ // ../qti-interactions/src/elements/qti-inline-choice/qti-inline-choice.styles.ts
6828
+ var styles2 = i`
6829
+ :host {
6830
+ display: block;
6831
+ box-sizing: border-box;
6832
+ }
6833
+
6834
+ slot {
6835
+ display: inline;
6836
+ }
6837
+ `;
6838
+ var qti_inline_choice_styles_default = styles2;
6839
+
7086
6840
  // ../qti-interactions/src/elements/qti-inline-choice/qti-inline-choice.ts
7087
- var QtiInlineChoice = class extends i2 {
7088
- static get styles() {
7089
- return [
7090
- i`
7091
- :host {
7092
- display: block;
7093
- cursor: pointer;
7094
- }
7095
- `
7096
- ];
6841
+ var QtiInlineChoice = class extends ActiveElementMixin(i2, "qti-inline-choice") {
6842
+ static {
6843
+ this.styles = qti_inline_choice_styles_default;
7097
6844
  }
7098
6845
  connectedCallback() {
7099
6846
  super.connectedCallback();
@@ -7857,4 +7604,4 @@ lit-html/node/directives/ref.js:
7857
7604
  * SPDX-License-Identifier: BSD-3-Clause
7858
7605
  *)
7859
7606
  */
7860
- //# sourceMappingURL=chunk-NUNAE73U.js.map
7607
+ //# sourceMappingURL=chunk-2X7747Q4.js.map