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