@qti-components/portable-custom-interaction 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1848 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { css, html } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { consume } from '@lit/context';
10
+ import { Interaction, itemContext, qtiContext, removeDoubleSlashes } from '@qti-components/base';
11
+ import styles from './qti-portable-custom-interaction.styles';
12
+ export class QtiPortableCustomInteraction extends Interaction {
13
+ constructor() {
14
+ super(...arguments);
15
+ this._iframeLoaded = false;
16
+ this._pendingMessages = [];
17
+ this._iframeMessageOrigin = null;
18
+ this._iframeObjectUrl = null;
19
+ this.requirePathsJson = '';
20
+ this.requireShimJson = '';
21
+ this.requireJsUrl = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js';
22
+ this.baseUrl = '';
23
+ this.useDefaultPaths = false;
24
+ this._errorMessage = null;
25
+ this.response = null;
26
+ this.#parsedRequirePaths = null;
27
+ this.#parsedRequireShim = null;
28
+ this.#pciValidity = null;
29
+ this.#pciCustomValidityMessage = '';
30
+ /**
31
+ * IFRAME MODE: Handle iframe messages
32
+ */
33
+ this.handleIframeMessage = (event) => {
34
+ const { data } = event;
35
+ // Ensure the message is from our iframe
36
+ if (!data || data.source !== 'qti-pci-iframe') {
37
+ return;
38
+ }
39
+ if (!this.iframe?.contentWindow || event.source !== this.iframe.contentWindow) {
40
+ return;
41
+ }
42
+ if (data.responseIdentifier && data.responseIdentifier !== this.responseIdentifier) {
43
+ return;
44
+ }
45
+ if (this._iframeMessageOrigin === null) {
46
+ this._iframeMessageOrigin = event.origin;
47
+ }
48
+ else if (event.origin !== this._iframeMessageOrigin) {
49
+ return;
50
+ }
51
+ switch (data.method) {
52
+ case 'iframeReady':
53
+ this.#initializeInteraction();
54
+ this.#processPendingMessages();
55
+ this.dispatchEvent(new CustomEvent('qti-portable-custom-interaction-loaded', {
56
+ bubbles: true
57
+ }));
58
+ break;
59
+ case 'resize':
60
+ if (typeof data.height === 'number' && this.iframe) {
61
+ this.iframe.style.height = `${data.height}px`;
62
+ this.iframe.style.width = `${data.width}px`;
63
+ }
64
+ break;
65
+ case 'console':
66
+ this.dispatchEvent(new CustomEvent('qti-pci-console', {
67
+ detail: {
68
+ level: data.level,
69
+ args: data.args
70
+ },
71
+ bubbles: true
72
+ }));
73
+ break;
74
+ case 'interactionChanged': {
75
+ const raw = data?.params?.value;
76
+ const converted = raw && typeof raw === 'object' ? this.#convertQtiVariableJSON(raw) : null;
77
+ const pciValid = typeof data?.params?.valid === 'boolean' ? data.params.valid : null;
78
+ const pciCustomValidity = typeof data?.params?.customValidity === 'string' ? data.params.customValidity : null;
79
+ if (pciValid !== null) {
80
+ this.#pciValidity = pciValid;
81
+ if (pciValid && pciCustomValidity === null) {
82
+ this.#pciCustomValidityMessage = '';
83
+ }
84
+ }
85
+ if (pciCustomValidity !== null) {
86
+ this.#pciCustomValidityMessage = pciCustomValidity;
87
+ }
88
+ // PCI state "should" be an opaque string, but a lot of existing PCIs (including the
89
+ // IMS conformance examples) return a structured object from getState().
90
+ //
91
+ // We store a string in itemContext.state, so we serialize non-string states with a
92
+ // prefix to safely round-trip them without accidentally parsing user-provided strings.
93
+ const stateRaw = data?.params?.state;
94
+ let state;
95
+ if (stateRaw === undefined) {
96
+ state = undefined;
97
+ }
98
+ else if (stateRaw === null) {
99
+ state = null;
100
+ }
101
+ else if (typeof stateRaw === 'string') {
102
+ state = stateRaw;
103
+ }
104
+ else {
105
+ try {
106
+ state = `__qti_json__::${JSON.stringify(stateRaw)}`;
107
+ }
108
+ catch {
109
+ state = null;
110
+ }
111
+ }
112
+ // Treat null/undefined or unconvertible responses as "cleared".
113
+ // The iframe side is responsible for not emitting an initial clear on load.
114
+ if (converted === null) {
115
+ const emptyResponse = this.responseVariable?.cardinality === 'single' ? '' : [];
116
+ this.response = emptyResponse;
117
+ this.validate();
118
+ this.saveResponse(emptyResponse, state);
119
+ break;
120
+ }
121
+ this.response = converted;
122
+ this.validate();
123
+ this.saveResponse(converted, state);
124
+ break;
125
+ }
126
+ case 'error':
127
+ this._errorMessage = data.params.message;
128
+ console.error('Error from PCI iframe:', data.params.message);
129
+ break;
130
+ }
131
+ };
132
+ // Add this property to store the previous state
133
+ this.#previousState = null;
134
+ }
135
+ #value;
136
+ // This implementation always renders inside an iframe.
137
+ static { this.styles = [
138
+ styles,
139
+ css `
140
+ :host {
141
+ display: block;
142
+ width: 100%;
143
+ min-height: 50px;
144
+ }
145
+ `
146
+ ]; }
147
+ #parsedRequirePaths;
148
+ #parsedRequireShim;
149
+ #pciValidity;
150
+ #pciCustomValidityMessage;
151
+ /**
152
+ * Parse the require paths JSON string into an object
153
+ */
154
+ #getRequirePaths() {
155
+ if (this.#parsedRequirePaths === null && this.requirePathsJson) {
156
+ try {
157
+ const parsedJson = JSON.parse(this.requirePathsJson);
158
+ if (Array.isArray(parsedJson) || !parsedJson || typeof parsedJson !== 'object') {
159
+ throw new Error('Require paths must be a JSON object.');
160
+ }
161
+ this.#parsedRequirePaths = parsedJson;
162
+ }
163
+ catch (e) {
164
+ console.error('Error parsing require paths JSON:', e);
165
+ this._errorMessage = `Error parsing require paths JSON: ${e.message}`;
166
+ this.#parsedRequirePaths = {};
167
+ }
168
+ }
169
+ return this.#parsedRequirePaths || {};
170
+ }
171
+ /**
172
+ * Parse the require shim JSON string into an object
173
+ */
174
+ #getRequireShim() {
175
+ if (this.#parsedRequireShim === null && this.requireShimJson) {
176
+ try {
177
+ this.#parsedRequireShim = JSON.parse(this.requireShimJson);
178
+ }
179
+ catch (e) {
180
+ console.error('Error parsing require shim JSON:', e);
181
+ this._errorMessage = `Error parsing require shim JSON: ${e.message}`;
182
+ this.#parsedRequireShim = {};
183
+ }
184
+ }
185
+ return this.#parsedRequireShim || {};
186
+ }
187
+ /**
188
+ * Get the final require paths by combining defaults with user-provided paths
189
+ */
190
+ #getFinalRequirePaths() {
191
+ // Return only user-provided paths to keep PCI completely self-contained
192
+ return this.#getRequirePaths();
193
+ }
194
+ /**
195
+ * Get the final require shim
196
+ */
197
+ #getFinalRequireShim() {
198
+ return this.#getRequireShim();
199
+ }
200
+ /**
201
+ * Converts QtiVariableJSON to a string or string array
202
+ */
203
+ #convertQtiVariableJSON(input) {
204
+ for (const topLevelKey in input) {
205
+ // eslint-disable-next-line no-prototype-builtins
206
+ if (input.hasOwnProperty(topLevelKey)) {
207
+ const nestedObject = input[topLevelKey];
208
+ if (nestedObject) {
209
+ for (const nestedKey in nestedObject) {
210
+ // eslint-disable-next-line no-prototype-builtins
211
+ if (nestedObject.hasOwnProperty(nestedKey)) {
212
+ const value = nestedObject[nestedKey];
213
+ if (Array.isArray(value)) {
214
+ return value.map(String); // Convert each element in the array to string
215
+ }
216
+ else if (value !== undefined && value !== null) {
217
+ return String(value); // Convert the single value to string
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+ /**
227
+ * Get direct child elements by tag name
228
+ */
229
+ getDirectChildrenByTag(tagName) {
230
+ const needle = tagName.toLowerCase();
231
+ return Array.from(this.children).filter(child => child.tagName.toLowerCase() === needle);
232
+ }
233
+ /**
234
+ * Coerce a scalar value to the expected base-type when possible
235
+ */
236
+ coerceBaseValue(value, baseType) {
237
+ if (value === null || value === undefined)
238
+ return value;
239
+ switch (baseType) {
240
+ case 'integer': {
241
+ if (typeof value === 'number')
242
+ return value;
243
+ const parsed = parseInt(String(value), 10);
244
+ return Number.isNaN(parsed) ? value : parsed;
245
+ }
246
+ case 'float':
247
+ case 'duration': {
248
+ if (typeof value === 'number')
249
+ return value;
250
+ const parsed = parseFloat(String(value));
251
+ return Number.isNaN(parsed) ? value : parsed;
252
+ }
253
+ case 'boolean': {
254
+ if (typeof value === 'boolean')
255
+ return value;
256
+ if (typeof value === 'string') {
257
+ const lowered = value.toLowerCase();
258
+ if (lowered === 'true')
259
+ return true;
260
+ if (lowered === 'false')
261
+ return false;
262
+ }
263
+ return value;
264
+ }
265
+ case 'string':
266
+ case 'identifier':
267
+ default:
268
+ return String(value);
269
+ }
270
+ }
271
+ /**
272
+ * Normalize an input into a list form
273
+ */
274
+ normalizeListValue(value) {
275
+ if (value === null || value === undefined)
276
+ return [];
277
+ return Array.isArray(value) ? value : [value];
278
+ }
279
+ /**
280
+ * Convert a variable value to QtiVariableJSON
281
+ */
282
+ variableToQtiVariableJSON(value, cardinality, baseType) {
283
+ if (cardinality === 'record' || baseType === 'record') {
284
+ return this.recordToQtiVariableJSON(value);
285
+ }
286
+ if (cardinality !== 'single') {
287
+ const listValues = this.normalizeListValue(value).map(v => this.coerceBaseValue(v, baseType));
288
+ return { list: { [baseType]: listValues } };
289
+ }
290
+ return { base: { [baseType]: this.coerceBaseValue(value, baseType) } };
291
+ }
292
+ /**
293
+ * Convert a record object into QtiVariableJSON
294
+ */
295
+ recordToQtiVariableJSON(value) {
296
+ if (!value || typeof value !== 'object') {
297
+ return { record: [] };
298
+ }
299
+ const recordEntries = [];
300
+ for (const [name, entryValue] of Object.entries(value)) {
301
+ recordEntries.push(this.recordEntryFromValue(name, entryValue));
302
+ }
303
+ return { record: recordEntries };
304
+ }
305
+ /**
306
+ * Build a record entry from an arbitrary value
307
+ */
308
+ recordEntryFromValue(name, entryValue) {
309
+ if (Array.isArray(entryValue)) {
310
+ const filtered = entryValue.filter(v => v !== null && v !== undefined);
311
+ if (filtered.length && filtered.every(v => typeof v === 'boolean')) {
312
+ return { name, list: { boolean: filtered } };
313
+ }
314
+ if (filtered.length && filtered.every(v => typeof v === 'number')) {
315
+ const numbers = filtered;
316
+ const isIntegers = numbers.every(n => Number.isInteger(n));
317
+ return { name, list: { [isIntegers ? 'integer' : 'float']: numbers } };
318
+ }
319
+ return { name, list: { string: entryValue.map(v => (v === null || v === undefined ? '' : String(v))) } };
320
+ }
321
+ if (typeof entryValue === 'boolean') {
322
+ return { name, base: { boolean: entryValue } };
323
+ }
324
+ if (typeof entryValue === 'number') {
325
+ if (Number.isInteger(entryValue))
326
+ return { name, base: { integer: entryValue } };
327
+ return { name, base: { float: entryValue } };
328
+ }
329
+ if (entryValue === null || entryValue === undefined) {
330
+ return { name, base: { string: '' } };
331
+ }
332
+ if (typeof entryValue === 'object') {
333
+ try {
334
+ return { name, base: { string: JSON.stringify(entryValue) } };
335
+ }
336
+ catch {
337
+ return { name, base: { string: String(entryValue) } };
338
+ }
339
+ }
340
+ return { name, base: { string: String(entryValue) } };
341
+ }
342
+ /**
343
+ * Extract default QTI_CONTEXT values from qti-context-declaration elements
344
+ */
345
+ getQtiContextDefaultsFromItem() {
346
+ const itemElement = this.closest('qti-assessment-item');
347
+ if (!itemElement)
348
+ return {};
349
+ const defaults = {};
350
+ const contextDeclarations = itemElement.querySelectorAll('qti-context-declaration[identifier="QTI_CONTEXT"]');
351
+ contextDeclarations.forEach(declaration => {
352
+ const defaultValueElement = declaration.querySelector('qti-default-value');
353
+ if (!defaultValueElement)
354
+ return;
355
+ const valueElements = defaultValueElement.querySelectorAll('qti-value[field-identifier]');
356
+ valueElements.forEach(valueElement => {
357
+ const fieldIdentifier = valueElement.getAttribute('field-identifier');
358
+ if (!fieldIdentifier)
359
+ return;
360
+ const baseType = (valueElement.getAttribute('base-type') || 'string');
361
+ const textContent = valueElement.textContent?.trim() || '';
362
+ defaults[fieldIdentifier] = this.coerceBaseValue(textContent, baseType);
363
+ });
364
+ });
365
+ return defaults;
366
+ }
367
+ /**
368
+ * Build a QTI_CONTEXT record for PCI consumption
369
+ */
370
+ getQtiContextRecord() {
371
+ const defaults = this.getQtiContextDefaultsFromItem();
372
+ const runtime = this.qtiContext?.QTI_CONTEXT || {};
373
+ const merged = { ...defaults, ...runtime };
374
+ if (!Object.keys(merged).length)
375
+ return null;
376
+ const normalized = {
377
+ candidateIdentifier: merged.candidateIdentifier ?? '',
378
+ testIdentifier: merged.testIdentifier ?? '',
379
+ environmentIdentifier: merged.environmentIdentifier ?? '',
380
+ ...merged
381
+ };
382
+ return this.recordToQtiVariableJSON(normalized);
383
+ }
384
+ /**
385
+ * Collect template variables referenced by qti-template-variable children
386
+ */
387
+ getTemplateVariables() {
388
+ const templateVariables = {};
389
+ const templateVariableElements = this.getDirectChildrenByTag('qti-template-variable');
390
+ if (!templateVariableElements.length)
391
+ return templateVariables;
392
+ templateVariableElements.forEach(el => {
393
+ const identifier = el.getAttribute('template-identifier') || el.getAttribute('identifier') || '';
394
+ if (!identifier)
395
+ return;
396
+ const variable = this.context?.variables?.find(v => v.identifier === identifier);
397
+ if (!variable)
398
+ return;
399
+ const cardinality = (variable.cardinality || 'single');
400
+ const baseType = (variable.baseType || 'string');
401
+ templateVariables[identifier] = this.variableToQtiVariableJSON(variable.value, cardinality, baseType);
402
+ });
403
+ return templateVariables;
404
+ }
405
+ /**
406
+ * Collect context variables referenced by qti-context-variable children
407
+ */
408
+ getContextVariables() {
409
+ const contextVariables = {};
410
+ const contextVariableElements = this.getDirectChildrenByTag('qti-context-variable');
411
+ if (!contextVariableElements.length)
412
+ return contextVariables;
413
+ contextVariableElements.forEach(el => {
414
+ const identifier = el.getAttribute('identifier') || '';
415
+ if (!identifier)
416
+ return;
417
+ if (identifier === 'QTI_CONTEXT') {
418
+ const record = this.getQtiContextRecord();
419
+ if (record)
420
+ contextVariables[identifier] = record;
421
+ return;
422
+ }
423
+ const variable = this.context?.variables?.find(v => v.identifier === identifier);
424
+ if (!variable)
425
+ return;
426
+ const cardinality = (variable.cardinality || 'single');
427
+ const baseType = (variable.baseType || 'string');
428
+ contextVariables[identifier] = this.variableToQtiVariableJSON(variable.value, cardinality, baseType);
429
+ });
430
+ return contextVariables;
431
+ }
432
+ /**
433
+ * Adds hyphenated versions of camelCase keys to properties object
434
+ */
435
+ #addHyphenatedKeys(properties) {
436
+ const updatedProperties = { ...properties };
437
+ for (const key in properties) {
438
+ if (Object.prototype.hasOwnProperty.call(properties, key)) {
439
+ const hyphenatedKey = key.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
440
+ updatedProperties[hyphenatedKey] = properties[key];
441
+ }
442
+ }
443
+ return updatedProperties;
444
+ }
445
+ #toNestedProperties(properties) {
446
+ const nestedProperties = { ...properties };
447
+ const setNestedValue = (root, path, value) => {
448
+ let cursor = root;
449
+ for (let i = 0; i < path.length; i += 1) {
450
+ const segment = path[i];
451
+ const isLast = i === path.length - 1;
452
+ if (isLast) {
453
+ cursor[segment] = value;
454
+ return;
455
+ }
456
+ if (!cursor[segment] || typeof cursor[segment] !== 'object' || Array.isArray(cursor[segment])) {
457
+ cursor[segment] = {};
458
+ }
459
+ cursor = cursor[segment];
460
+ }
461
+ };
462
+ for (const [key, value] of Object.entries(properties)) {
463
+ if (!key.includes('__'))
464
+ continue;
465
+ const path = key.split('__').filter(Boolean);
466
+ if (path.length < 2)
467
+ continue;
468
+ setNestedValue(nestedProperties, path, value);
469
+ }
470
+ return nestedProperties;
471
+ }
472
+ /**
473
+ * Converts response variables to QtiVariableJSON
474
+ */
475
+ responseVariablesToQtiVariableJSON(input, cardinality, baseType) {
476
+ if (cardinality !== 'single') {
477
+ const list = { list: {} };
478
+ list.list[baseType] = input;
479
+ return list;
480
+ }
481
+ else {
482
+ const base = { base: {} };
483
+ base.base[baseType] = input;
484
+ return base;
485
+ }
486
+ }
487
+ #hasNonEmptyResponse() {
488
+ if (this.response === null || this.response === undefined)
489
+ return false;
490
+ if (Array.isArray(this.response)) {
491
+ if (this.response.length === 0)
492
+ return false;
493
+ return this.response.some(v => v !== '' && v !== null && v !== undefined);
494
+ }
495
+ return this.response !== '';
496
+ }
497
+ #setInteractionValidity(isValid, customMessage) {
498
+ const fallbackMessage = this.dataset.invalidresponsemessage || 'Invalid response.';
499
+ const message = isValid ? '' : customMessage && customMessage.trim().length > 0 ? customMessage : fallbackMessage;
500
+ this._internals.setValidity(isValid ? {} : { customError: true }, message);
501
+ }
502
+ validate() {
503
+ const isValid = typeof this.#pciValidity === 'boolean' ? this.#pciValidity : this.#hasNonEmptyResponse();
504
+ this.#setInteractionValidity(isValid, this.#pciCustomValidityMessage);
505
+ return isValid;
506
+ }
507
+ set value(v) {
508
+ if (v === null) {
509
+ this.#value = [];
510
+ }
511
+ else {
512
+ this.#value = Array.isArray(v) ? v : v.split(',');
513
+ }
514
+ // PCI handles response setting via boundTo property during initialization
515
+ // No need to call setResponse directly
516
+ }
517
+ get value() {
518
+ if (this.#value === null || this.#value === undefined)
519
+ return null;
520
+ return Array.isArray(this.#value) ? this.#value.join(',') : String(this.#value);
521
+ }
522
+ set boundTo(newValue) {
523
+ if (!newValue || !newValue[this.responseIdentifier]) {
524
+ return;
525
+ }
526
+ const value = this.#convertQtiVariableJSON(newValue[this.responseIdentifier]);
527
+ this.#value = value;
528
+ // No direct call to setResponse - PCI will handle this during initialization
529
+ // through the boundTo property in the config
530
+ this.saveResponse(value);
531
+ }
532
+ get boundTo() {
533
+ const responseVariable = {};
534
+ const variable = this.context?.variables?.find(v => v.identifier === this.responseIdentifier);
535
+ if (variable) {
536
+ const cardinality = this.context?.variables?.find(v => v.identifier === this.responseIdentifier)?.cardinality || 'single';
537
+ const baseType = this.context?.variables?.find(v => v.identifier === this.responseIdentifier)?.baseType || 'string';
538
+ const value = this.context?.variables?.find(v => v.identifier === this.responseIdentifier)?.value || null;
539
+ const responseVal = this.responseVariablesToQtiVariableJSON(value, cardinality, baseType);
540
+ responseVariable[this.responseIdentifier] = responseVal;
541
+ }
542
+ return responseVariable;
543
+ }
544
+ /**
545
+ * Unescape HTML entities in a string
546
+ */
547
+ #unescapeHtml(str) {
548
+ if (!str)
549
+ return str;
550
+ return str
551
+ .replace(/&amp;/g, '&')
552
+ .replace(/&lt;/g, '<')
553
+ .replace(/&gt;/g, '>')
554
+ .replace(/&quot;/g, '"')
555
+ .replace(/&#x27;/g, "'")
556
+ .replace(/&#x2F;/g, '/')
557
+ .replace(/&#x60;/g, '`')
558
+ .replace(/&#x3D;/g, '=');
559
+ }
560
+ /**
561
+ * Unescape HTML entities in all values of an object
562
+ */
563
+ #unescapeDataAttributes(obj) {
564
+ const unescaped = {};
565
+ for (const [key, value] of Object.entries(obj)) {
566
+ if (typeof value === 'string') {
567
+ unescaped[key] = this.#unescapeHtml(value);
568
+ }
569
+ else {
570
+ unescaped[key] = value;
571
+ }
572
+ }
573
+ return unescaped;
574
+ }
575
+ disconnectedCallback() {
576
+ super.disconnectedCallback();
577
+ window.removeEventListener('message', this.handleIframeMessage);
578
+ if (this._iframeObjectUrl) {
579
+ URL.revokeObjectURL(this._iframeObjectUrl);
580
+ this._iframeObjectUrl = null;
581
+ }
582
+ }
583
+ /**
584
+ * IFRAME MODE: Send message to iframe
585
+ */
586
+ sendMessageToIframe(method, params) {
587
+ const targetWindow = this.iframe?.contentWindow;
588
+ if (!this._iframeLoaded || !targetWindow) {
589
+ this._pendingMessages.push({ method, params });
590
+ return;
591
+ }
592
+ targetWindow.postMessage({
593
+ source: 'qti-portable-custom-interaction',
594
+ responseIdentifier: this.responseIdentifier,
595
+ method,
596
+ params
597
+ }, '*');
598
+ }
599
+ /**
600
+ * IFRAME MODE: Process pending messages
601
+ */
602
+ #processPendingMessages() {
603
+ if (this._pendingMessages.length) {
604
+ this._pendingMessages.forEach(message => {
605
+ this.sendMessageToIframe(message.method, message.params);
606
+ });
607
+ this._pendingMessages = [];
608
+ }
609
+ }
610
+ /**
611
+ * IFRAME MODE: Create iframe element
612
+ */
613
+ createIframe() {
614
+ if (this._iframeObjectUrl) {
615
+ URL.revokeObjectURL(this._iframeObjectUrl);
616
+ this._iframeObjectUrl = null;
617
+ }
618
+ this.iframe = document.createElement('iframe');
619
+ this.iframe.id = `pci-iframe-${this.responseIdentifier}`;
620
+ this.iframe.setAttribute('title', 'QTI PCI Iframe');
621
+ this.iframe.setAttribute('aria-label', 'QTI PCI Iframe');
622
+ this.iframe.setAttribute('aria-hidden', 'false');
623
+ this.iframe.setAttribute('role', 'application');
624
+ this.iframe.style.width = '100%';
625
+ this.iframe.style.border = 'none';
626
+ this.iframe.style.display = 'block';
627
+ // Handle iframe load event
628
+ this.iframe.onload = () => {
629
+ this._iframeLoaded = true;
630
+ if (this._iframeObjectUrl) {
631
+ URL.revokeObjectURL(this._iframeObjectUrl);
632
+ this._iframeObjectUrl = null;
633
+ }
634
+ this.#addMarkupToIframe();
635
+ // Send initialization data to iframe
636
+ this.#sendIframeInitData();
637
+ };
638
+ // Create a unique name for the iframe
639
+ const iframeName = `qti-pci-${this.responseIdentifier}-${Date.now()}`;
640
+ this.iframe.name = iframeName;
641
+ // Generate iframe HTML content with all required scripts
642
+ const iframeContent = this.generateIframeContent();
643
+ // Prefer a same-origin blob URL to avoid postMessage target-origin mismatches (e.g. in Storybook).
644
+ try {
645
+ const blob = new Blob([iframeContent], { type: 'text/html' });
646
+ this._iframeObjectUrl = URL.createObjectURL(blob);
647
+ this.iframe.src = this._iframeObjectUrl;
648
+ }
649
+ catch {
650
+ const encodedContent = encodeURIComponent(iframeContent);
651
+ this.iframe.src = `data:text/html;charset=utf-8,${encodedContent}`;
652
+ }
653
+ // Append iframe to component
654
+ this.appendChild(this.iframe);
655
+ }
656
+ /**
657
+ * IFRAME MODE: Send initialization data to iframe
658
+ */
659
+ #sendIframeInitData() {
660
+ // Once iframe is loaded, send initialization data
661
+ const properties = this.#addHyphenatedKeys(this.#toNestedProperties(this.#unescapeDataAttributes({ ...this.dataset })));
662
+ const storedStateRaw = this.context?.state?.[this.responseIdentifier];
663
+ const storedState = typeof storedStateRaw === 'string' && storedStateRaw.length > 0 ? storedStateRaw : null;
664
+ const initData = {
665
+ module: this.module,
666
+ customInteractionTypeIdentifier: this.customInteractionTypeIdentifier,
667
+ baseUrl: !this.baseUrl
668
+ ? window.location.origin
669
+ : this.baseUrl.startsWith('http') || this.baseUrl.startsWith('blob') || this.baseUrl.startsWith('base64')
670
+ ? this.baseUrl
671
+ : removeDoubleSlashes(`${window.location.origin}${this.baseUrl}`),
672
+ responseIdentifier: this.responseIdentifier,
673
+ properties,
674
+ contextVariables: this.getContextVariables(),
675
+ templateVariables: this.getTemplateVariables(),
676
+ dataAttributes: { ...this.dataset },
677
+ interactionModules: this.#getInteractionModules(),
678
+ boundTo: storedState ? null : this.boundTo,
679
+ state: storedState
680
+ };
681
+ this.sendMessageToIframe('initialize', initData);
682
+ }
683
+ /**
684
+ * IFRAME MODE: Get interaction modules from DOM
685
+ */
686
+ #getInteractionModules() {
687
+ const modules = [];
688
+ const interactionModules = this.querySelector('qti-interaction-modules');
689
+ if (interactionModules) {
690
+ const moduleElements = interactionModules.querySelectorAll('qti-interaction-module');
691
+ for (const module of moduleElements) {
692
+ modules.push({
693
+ id: module.getAttribute('id'),
694
+ primaryPath: module.getAttribute('primary-path'),
695
+ fallbackPath: module.getAttribute('fallback-path')
696
+ });
697
+ }
698
+ }
699
+ return modules;
700
+ }
701
+ /**
702
+ * IFRAME MODE: Add markup, stylesheets, and properties to iframe
703
+ */
704
+ #addMarkupToIframe() {
705
+ // Get interaction markup if any
706
+ const markup = this.querySelector('qti-interaction-markup');
707
+ if (markup) {
708
+ this.sendMessageToIframe('setMarkup', markup.innerHTML);
709
+ }
710
+ const stylesheets = Array.from(this.querySelectorAll('qti-stylesheet'));
711
+ if (stylesheets.length) {
712
+ const stylesheetsHtml = stylesheets.map(sheet => sheet.outerHTML).join('\n');
713
+ this.sendMessageToIframe('setStylesheets', stylesheetsHtml);
714
+ }
715
+ // Get properties if any
716
+ const properties = this.querySelector('properties');
717
+ if (properties) {
718
+ this.sendMessageToIframe('setProperties', properties.innerHTML);
719
+ }
720
+ }
721
+ /**
722
+ * IFRAME MODE: Initialize the interaction
723
+ */
724
+ #initializeInteraction() {
725
+ // No explicit action needed, as the PCI will initialize
726
+ // with the boundTo property already provided
727
+ }
728
+ connectedCallback() {
729
+ super.connectedCallback();
730
+ this.#pciValidity = null;
731
+ this.#pciCustomValidityMessage = '';
732
+ // Reflect any pre-existing response (e.g. restored session) for validation/completionStatus.
733
+ this.response = this.responseVariable?.value ?? null;
734
+ window.addEventListener('message', this.handleIframeMessage);
735
+ this.createIframe();
736
+ }
737
+ /**
738
+ * IFRAME MODE: Generate iframe HTML content
739
+ */
740
+ generateIframeContent() {
741
+ const parentStyles = window.getComputedStyle(document.body);
742
+ // Get the configured require paths and shim
743
+ const requirePaths = JSON.stringify(this.#getFinalRequirePaths());
744
+ const requireShim = JSON.stringify(this.#getFinalRequireShim());
745
+ const iframeBaseUrl = this.dataset.baseUrl
746
+ ? this.dataset.baseUrl.startsWith('/')
747
+ ? `${window.location.origin}${this.dataset.baseUrl}`
748
+ : this.dataset.baseUrl
749
+ : '';
750
+ const forwardConsole = this.dataset.forwardConsole === 'true';
751
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
752
+ const defaultComponentsCdnUrl = isLocalhost
753
+ ? `${window.location.origin}/cdn/index.js`
754
+ : 'https://unpkg.com/@citolab/qti-components/cdn';
755
+ const defaultComponentsCssUrl = isLocalhost
756
+ ? `${window.location.origin}/dist/item.css`
757
+ : 'https://unpkg.com/@citolab/qti-components@latest/dist/item.css';
758
+ const componentsCdnUrl = this.dataset.componentsCdnUrl || defaultComponentsCdnUrl;
759
+ const componentsCssUrl = this.dataset.componentsCssUrl || defaultComponentsCssUrl;
760
+ // Extract just the font-related properties you want to copy
761
+ const fontStyles = `
762
+ font-family: ${parentStyles.getPropertyValue('font-family')};
763
+ font-size: ${parentStyles.getPropertyValue('font-size')};
764
+ line-height: ${parentStyles.getPropertyValue('line-height')};
765
+ font-weight: ${parentStyles.getPropertyValue('font-weight')};
766
+ color: ${parentStyles.getPropertyValue('color')};
767
+ `;
768
+ return /* html */ `<!DOCTYPE html>
769
+ <html lang="en">
770
+ <head>
771
+ <meta charset="utf-8" />
772
+ <title>QTI PCI Container</title>
773
+ <base href="${window.location.origin}" />
774
+ <script type="module">
775
+ import '${componentsCdnUrl}';
776
+ </script>
777
+ <style>
778
+ body, html {
779
+ margin: 0;
780
+ padding: 0;
781
+ width: 100%;
782
+ height: auto;
783
+ overflow: hidden;
784
+ /* Add the extracted font styles here */
785
+ ${fontStyles}
786
+ };
787
+ .qti-customInteraction {
788
+ width: 100%;
789
+ height: 100%;
790
+ }
791
+ #pci-container {
792
+ width: 100%;
793
+ }
794
+ qti-interaction-markup {
795
+ display: block;
796
+ width: 100%;
797
+ min-height: 50px;
798
+ }
799
+ </style>
800
+ <link href="${componentsCssUrl}" rel="stylesheet" />
801
+ <script src="${this.requireJsUrl}"></script>
802
+ <script>
803
+ const forwardConsole = ${forwardConsole ? 'true' : 'false'};
804
+ if (forwardConsole) {
805
+ const originalLog = console.log.bind(console);
806
+ const originalError = console.error.bind(console);
807
+ const stringifyArgs = args =>
808
+ args.map(arg => {
809
+ if (typeof arg === 'string') return arg;
810
+ try {
811
+ return JSON.stringify(arg);
812
+ } catch (e) {
813
+ return String(arg);
814
+ }
815
+ });
816
+ console.log = (...args) => {
817
+ originalLog(...args);
818
+ window.parent.postMessage(
819
+ {
820
+ source: 'qti-pci-iframe',
821
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
822
+ method: 'console',
823
+ level: 'log',
824
+ args: stringifyArgs(args)
825
+ },
826
+ '*'
827
+ );
828
+ };
829
+ console.error = (...args) => {
830
+ originalError(...args);
831
+ window.parent.postMessage(
832
+ {
833
+ source: 'qti-pci-iframe',
834
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
835
+ method: 'console',
836
+ level: 'error',
837
+ args: stringifyArgs(args)
838
+ },
839
+ '*'
840
+ );
841
+ };
842
+ }
843
+ // Define standard paths and shims
844
+ window.requirePaths = ${requirePaths};
845
+
846
+ window.requireShim = ${requireShim};
847
+
848
+ // Single initial RequireJS configuration with error handling
849
+ window.requirejs.config({
850
+ catchError: true,
851
+ waitSeconds: 30,
852
+ paths: window.requirePaths,
853
+ baseUrl: '${iframeBaseUrl}',
854
+ shim: window.requireShim,
855
+ onNodeCreated: function (node, config, moduleName, url) {
856
+ // Add error handler to script node
857
+ node.addEventListener('error', function (evt) {
858
+ console.error('Script load error for module:', moduleName, 'URL:', url, 'Event:', evt);
859
+ });
860
+ },
861
+ onError: function (err) {
862
+ console.error('RequireJS error:', {
863
+ type: err.requireType,
864
+ modules: err.requireModules,
865
+ error: err
866
+ });
867
+
868
+ if (err.requireType === 'scripterror') {
869
+ console.error('Script error usually indicates a network or CORS issue with:', err.requireModules);
870
+ }
871
+
872
+ // Notify parent window about the error
873
+ window.parent.postMessage(
874
+ {
875
+ source: 'qti-pci-iframe',
876
+ responseIdentifier: (window.PCIManager && window.PCIManager.responseIdentifier) || null,
877
+ method: 'error',
878
+ params: {
879
+ message: 'RequireJS ' + err.requireType + ' error for modules: ' + err.requireModules,
880
+ details: {
881
+ type: err.requireType,
882
+ modules: err.requireModules,
883
+ error: err.toString()
884
+ }
885
+ }
886
+ },
887
+ '*'
888
+ );
889
+ }
890
+ });
891
+
892
+ // PCI Manager for iframe implementation
893
+ window.PCIManager = {
894
+ pciInstance: null,
895
+ container: null,
896
+ markupEl: null,
897
+ propertiesEl: null,
898
+ customInteractionTypeIdentifier: null,
899
+ responseIdentifier: null,
900
+ pendingBoundTo: null,
901
+ pendingMarkup: null,
902
+ pendingProperties: null,
903
+ pendingState: null,
904
+ pendingStylesheets: null,
905
+ stylesheetKeys: {},
906
+ interactionChangedViaEvent: false,
907
+ eventBridgeAttached: false,
908
+ lastResponseStr: null,
909
+ hadResponse: false,
910
+
911
+ initialize: function (config) {
912
+ this.customInteractionTypeIdentifier = config.customInteractionTypeIdentifier;
913
+ this.responseIdentifier = config.responseIdentifier;
914
+ this.container = document.getElementById('pci-container');
915
+ this.container.classList.add('qti-customInteraction');
916
+
917
+ function qtiVariableHasValue(qtiVar) {
918
+ if (!qtiVar) return false;
919
+ if (qtiVar.base) {
920
+ for (const k in qtiVar.base) {
921
+ if (!Object.prototype.hasOwnProperty.call(qtiVar.base, k)) continue;
922
+ const v = qtiVar.base[k];
923
+ if (v !== null && v !== undefined && v !== '') return true;
924
+ }
925
+ }
926
+ if (qtiVar.list) {
927
+ for (const k in qtiVar.list) {
928
+ if (!Object.prototype.hasOwnProperty.call(qtiVar.list, k)) continue;
929
+ const v = qtiVar.list[k];
930
+ if (Array.isArray(v) && v.some(x => x !== null && x !== undefined && x !== '')) return true;
931
+ }
932
+ }
933
+ if (Array.isArray(qtiVar.record) && qtiVar.record.length > 0) return true;
934
+ return false;
935
+ }
936
+
937
+ const initialBoundTo = config.boundTo && config.boundTo[this.responseIdentifier];
938
+ this.hadResponse = qtiVariableHasValue(initialBoundTo);
939
+ this.lastResponseStr = this.hadResponse ? JSON.stringify(initialBoundTo) : null;
940
+ // Ensure expected DOM structure exists (markup + properties)
941
+ this.markupEl = this.container.querySelector('qti-interaction-markup');
942
+ if (!this.markupEl) {
943
+ this.markupEl = document.createElement('qti-interaction-markup');
944
+ this.container.appendChild(this.markupEl);
945
+ }
946
+ this.markupEl.classList.add('qti-customInteraction');
947
+ this.propertiesEl = this.container.querySelector('properties');
948
+ if (!this.propertiesEl) {
949
+ this.propertiesEl = document.createElement('properties');
950
+ this.propertiesEl.style.display = 'none';
951
+ this.container.appendChild(this.propertiesEl);
952
+ } else {
953
+ this.propertiesEl.style.display = 'none';
954
+ }
955
+
956
+ // Apply any markup/properties that arrived before initialization
957
+ if (this.pendingMarkup !== null) {
958
+ this.setMarkup(this.pendingMarkup);
959
+ this.pendingMarkup = null;
960
+ }
961
+ if (this.pendingProperties !== null) {
962
+ this.setProperties(this.pendingProperties);
963
+ this.pendingProperties = null;
964
+ }
965
+ if (this.pendingStylesheets !== null) {
966
+ this.setStylesheets(this.pendingStylesheets);
967
+ this.pendingStylesheets = null;
968
+ }
969
+
970
+ // Bridge qti-interaction-changed events (preferred over polling)
971
+ if (!this.eventBridgeAttached) {
972
+ this.eventBridgeAttached = true;
973
+ const self = this;
974
+ this.container.addEventListener(
975
+ 'qti-interaction-changed',
976
+ function (evt) {
977
+ try {
978
+ self.interactionChangedViaEvent = true;
979
+ const value = evt && evt.detail ? evt.detail.value : undefined;
980
+ if (value !== undefined) {
981
+ const validFromEvent =
982
+ evt && evt.detail && typeof evt.detail.valid === 'boolean' ? evt.detail.valid : undefined;
983
+ let valid = validFromEvent;
984
+ if (valid === undefined && self.pciInstance && typeof self.pciInstance.checkValidity === 'function') {
985
+ try {
986
+ valid = self.pciInstance.checkValidity();
987
+ } catch (e) {
988
+ // Keep undefined on PCI validity read errors.
989
+ }
990
+ }
991
+ const customValidityFromEvent =
992
+ evt && evt.detail && typeof evt.detail.customValidity === 'string'
993
+ ? evt.detail.customValidity
994
+ : undefined;
995
+ let customValidity = customValidityFromEvent;
996
+ if (
997
+ customValidity === undefined &&
998
+ self.pciInstance &&
999
+ typeof self.pciInstance.getCustomValidity === 'function'
1000
+ ) {
1001
+ try {
1002
+ customValidity = self.pciInstance.getCustomValidity();
1003
+ } catch (e) {
1004
+ // Keep undefined on PCI custom validity read errors.
1005
+ }
1006
+ }
1007
+ const state =
1008
+ self.pciInstance && typeof self.pciInstance.getState === 'function'
1009
+ ? self.pciInstance.getState()
1010
+ : null;
1011
+ self.notifyInteractionChanged(value, state, valid, customValidity);
1012
+ }
1013
+ } catch (e) {
1014
+ // ignore bridge errors, polling fallback may still work
1015
+ }
1016
+ },
1017
+ true
1018
+ );
1019
+ }
1020
+
1021
+ function getResolvablePath(path, basePath) {
1022
+ if (Array.isArray(path)) {
1023
+ return path.map(p => getResolvablePathString(p, basePath));
1024
+ } else {
1025
+ return getResolvablePathString(path, basePath);
1026
+ }
1027
+ }
1028
+
1029
+ function removeDoubleSlashes(str) {
1030
+ return str
1031
+ .replace(/([^:\\/])\\/\\/+/g, '$1/')
1032
+ .replace(/\\/\\//g, '/')
1033
+ .replace('http:/', 'http://')
1034
+ .replace('https:/', 'https://');
1035
+ }
1036
+
1037
+ function ensureDirectoryBasePath(basePath) {
1038
+ if (!basePath) return basePath;
1039
+ if (basePath.endsWith('/')) return basePath;
1040
+ return basePath + '/';
1041
+ }
1042
+
1043
+
1044
+
1045
+ function combineRequireResolvePaths(path1, path2, baseUrl) {
1046
+ path1 = getResolvablePath(path1, baseUrl);
1047
+ const path1Array = Array.isArray(path1) ? path1 : [path1];
1048
+ if (!path2) {
1049
+ return path1Array;
1050
+ }
1051
+ path2 = getResolvablePath(path2, baseUrl);
1052
+ const path2Array = Array.isArray(path2) ? path2 : [path2];
1053
+ return path1Array.concat(path2Array).filter((value, index, self) => self.indexOf(value) === index);
1054
+ }
1055
+
1056
+ // Update paths with modules from the config
1057
+ if (config.interactionModules && config.interactionModules.length > 0) {
1058
+ config.interactionModules.forEach(module => {
1059
+ if (module.id && module.primaryPath) {
1060
+ const currentPath = window.requirePaths[module.id] || [];
1061
+ const currentPaths = Array.isArray(currentPath) ? currentPath : [currentPath];
1062
+ const newPath = combineRequireResolvePaths(
1063
+ module.primaryPath,
1064
+ module.fallbackPath,
1065
+ config.baseUrl
1066
+ );
1067
+ window.requirePaths[module.id] = newPath.concat(currentPaths)
1068
+ .filter((value, index, self) => self.indexOf(value) === index);
1069
+ }
1070
+ });
1071
+ }
1072
+
1073
+ // The ONLY other requirejs.config call - with the context for this specific PCI
1074
+ window.requirejs.config({
1075
+ context: this.customInteractionTypeIdentifier,
1076
+ paths: window.requirePaths,
1077
+ shim: window.requireShim
1078
+ });
1079
+
1080
+ // Define qtiCustomInteractionContext for the PCI
1081
+ define('qtiCustomInteractionContext', () => {
1082
+ return {
1083
+ register: pciInstance => {
1084
+ this.pciInstance = pciInstance;
1085
+ // Configure PCI instance
1086
+ const pciConfig = {
1087
+ properties: config.properties || {},
1088
+ contextVariables: config.contextVariables || {},
1089
+ templateVariables: config.templateVariables || {},
1090
+ onready: pciInstance => {
1091
+ this.pciInstance = pciInstance;
1092
+ // Apply any pending updates that arrived before onready
1093
+ if (this.pendingBoundTo) {
1094
+ this.applyBoundTo(this.pendingBoundTo);
1095
+ this.pendingBoundTo = null;
1096
+ }
1097
+ if (this.pendingState && typeof this.pciInstance.setState === 'function') {
1098
+ this.pciInstance.setState(this.pendingState);
1099
+ this.pendingState = null;
1100
+ }
1101
+ this.notifyReady();
1102
+ },
1103
+ ondone: (pciInstance, response, state, status) => {
1104
+ const valid =
1105
+ this.pciInstance && typeof this.pciInstance.checkValidity === 'function'
1106
+ ? this.pciInstance.checkValidity()
1107
+ : undefined;
1108
+ const customValidity =
1109
+ this.pciInstance && typeof this.pciInstance.getCustomValidity === 'function'
1110
+ ? this.pciInstance.getCustomValidity()
1111
+ : undefined;
1112
+ this.notifyInteractionChanged(
1113
+ response,
1114
+ typeof state === 'string' ? state : null,
1115
+ valid,
1116
+ customValidity
1117
+ );
1118
+ },
1119
+ responseIdentifier: config.responseIdentifier,
1120
+ boundTo: config.boundTo
1121
+ };
1122
+
1123
+ if (pciInstance.getInstance) {
1124
+ const dom = this.markupEl || this.container;
1125
+ // Round-trip support for object states (stored as a prefixed JSON string by the host).
1126
+ // For strict string-based PCIs we pass the original string through unchanged.
1127
+ let restoredState = config.state;
1128
+ if (typeof restoredState === 'string' && restoredState.indexOf('__qti_json__::') === 0) {
1129
+ try {
1130
+ restoredState = JSON.parse(restoredState.substring('__qti_json__::'.length));
1131
+ } catch (e) {
1132
+ // If parsing fails, fall back to the raw string.
1133
+ restoredState = config.state;
1134
+ }
1135
+ }
1136
+ pciInstance.getInstance(dom, pciConfig, restoredState || undefined);
1137
+ } else {
1138
+ this.notifyError('Loaded PCI module has no getInstance().');
1139
+ }
1140
+ },
1141
+ notifyReady: () => {
1142
+ PCIManager.notifyReady();
1143
+ }
1144
+ };
1145
+ });
1146
+
1147
+ function getResolvablePathString(path, basePath) {
1148
+ path = path.replace(/\\.js$/, '');
1149
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || !basePath) {
1150
+ return path;
1151
+ }
1152
+
1153
+ if (path.startsWith('/') && !path.startsWith('//')) {
1154
+ path = path.slice(1);
1155
+ }
1156
+
1157
+ try {
1158
+ const resolved = new URL(path, ensureDirectoryBasePath(basePath)).toString();
1159
+ return resolved.replace(/\\.js$/, '');
1160
+ } catch (error) {
1161
+ return removeDoubleSlashes(ensureDirectoryBasePath(basePath) + path);
1162
+ }
1163
+ }
1164
+
1165
+ // Load the PCI module
1166
+ this.loadModule(config.module);
1167
+ },
1168
+
1169
+ loadModule: function (modulePath) {
1170
+ try {
1171
+ // Get the context-specific require
1172
+ const contextRequire = window.requirejs.config({
1173
+ context: this.customInteractionTypeIdentifier
1174
+ });
1175
+ contextRequire(['require'], require => {
1176
+ // Now load the actual module
1177
+ require([modulePath], () => {}, err => {
1178
+ console.error('Error loading module:', modulePath, err);
1179
+ this.notifyError('Module load error: ' + err.toString());
1180
+ });
1181
+ });
1182
+ } catch (error) {
1183
+ console.error('Exception in loadModule:', modulePath);
1184
+ console.error(error);
1185
+ this.notifyError('Error in require call: ' + error.toString());
1186
+ }
1187
+ },
1188
+
1189
+ notifyReady: function () {
1190
+ window.parent.postMessage(
1191
+ {
1192
+ source: 'qti-pci-iframe',
1193
+ responseIdentifier: this.responseIdentifier,
1194
+ method: 'iframeReady'
1195
+ },
1196
+ '*'
1197
+ );
1198
+ },
1199
+
1200
+ notifyInteractionChanged: function (response, state, valid, customValidity) {
1201
+ window.parent.postMessage(
1202
+ {
1203
+ source: 'qti-pci-iframe',
1204
+ responseIdentifier: this.responseIdentifier,
1205
+ method: 'interactionChanged',
1206
+ params: {
1207
+ value: response,
1208
+ state: state,
1209
+ valid: typeof valid === 'boolean' ? valid : undefined,
1210
+ customValidity: typeof customValidity === 'string' ? customValidity : undefined
1211
+ }
1212
+ },
1213
+ '*'
1214
+ );
1215
+ },
1216
+
1217
+ notifyError: function (message) {
1218
+ console.error('PCI Error:', message);
1219
+ window.parent.postMessage(
1220
+ {
1221
+ source: 'qti-pci-iframe',
1222
+ responseIdentifier: this.responseIdentifier,
1223
+ method: 'error',
1224
+ params: { message: message }
1225
+ },
1226
+ '*'
1227
+ );
1228
+ },
1229
+
1230
+ setMarkup: function (markupHtml) {
1231
+ if (!this.container) {
1232
+ this.container = document.getElementById('pci-container');
1233
+ }
1234
+ if (!this.container) {
1235
+ this.pendingMarkup = markupHtml;
1236
+ return;
1237
+ }
1238
+ this.markupEl = this.container.querySelector('qti-interaction-markup');
1239
+ if (!this.markupEl) {
1240
+ this.markupEl = document.createElement('qti-interaction-markup');
1241
+ this.container.appendChild(this.markupEl);
1242
+ }
1243
+ this.markupEl.classList.add('qti-customInteraction');
1244
+ this.markupEl.innerHTML = markupHtml || '';
1245
+ },
1246
+
1247
+ setProperties: function (propertiesHtml) {
1248
+ if (!this.container) {
1249
+ this.container = document.getElementById('pci-container');
1250
+ }
1251
+ if (!this.container) {
1252
+ this.pendingProperties = propertiesHtml;
1253
+ return;
1254
+ }
1255
+ this.propertiesEl = this.container.querySelector('properties');
1256
+ if (!this.propertiesEl) {
1257
+ this.propertiesEl = document.createElement('properties');
1258
+ this.container.appendChild(this.propertiesEl);
1259
+ }
1260
+ this.propertiesEl.style.display = 'none';
1261
+ this.propertiesEl.innerHTML = propertiesHtml || '';
1262
+ },
1263
+
1264
+ minifyCss: function (cssContent) {
1265
+ return cssContent
1266
+ .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')
1267
+ .replace(/\\s+/g, ' ')
1268
+ .replace(/\\s*([{}:;])\\s*/g, '$1')
1269
+ .trim();
1270
+ },
1271
+
1272
+ setStylesheets: function (stylesheetsHtml) {
1273
+ if (!stylesheetsHtml) return;
1274
+ if (!this.container) {
1275
+ this.container = document.getElementById('pci-container');
1276
+ }
1277
+ const target = this.container || document.body;
1278
+ if (!target) return;
1279
+ target.insertAdjacentHTML('afterbegin', stylesheetsHtml);
1280
+ },
1281
+
1282
+
1283
+ applyBoundTo: function (boundTo) {
1284
+ if (!this.pciInstance || typeof this.pciInstance.setResponse !== 'function') return;
1285
+ const value = boundTo && (boundTo[this.responseIdentifier] || boundTo[Object.keys(boundTo)[0]]);
1286
+ if (value) this.pciInstance.setResponse(value);
1287
+ }
1288
+ };
1289
+
1290
+ // Set up message listener for communication with parent
1291
+ let expectedParentOrigin = null;
1292
+ window.addEventListener('message', function (event) {
1293
+ const { data } = event;
1294
+
1295
+ // Ensure the message is from our parent
1296
+ if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') {
1297
+ return;
1298
+ }
1299
+ if (expectedParentOrigin === null) {
1300
+ expectedParentOrigin = event.origin;
1301
+ } else if (event.origin !== expectedParentOrigin) {
1302
+ return;
1303
+ }
1304
+
1305
+ function deepQuerySelector(root, selector) {
1306
+ if (!root) return null;
1307
+ try {
1308
+ const direct = root.querySelector ? root.querySelector(selector) : null;
1309
+ if (direct) return direct;
1310
+ } catch (e) {
1311
+ // ignore invalid selector for this root
1312
+ }
1313
+ if (!root.querySelectorAll) return null;
1314
+ const nodes = root.querySelectorAll('*');
1315
+ for (const node of nodes) {
1316
+ if (node && node.shadowRoot) {
1317
+ const found = deepQuerySelector(node.shadowRoot, selector);
1318
+ if (found) return found;
1319
+ }
1320
+ }
1321
+ return null;
1322
+ }
1323
+
1324
+ function deepFindElementByExactText(root, text) {
1325
+ if (!root || !text) return null;
1326
+ if (root.querySelectorAll) {
1327
+ const nodes = root.querySelectorAll('*');
1328
+ for (const node of nodes) {
1329
+ if ((node.textContent || '').trim() === text) return node;
1330
+ if (node.shadowRoot) {
1331
+ const found = deepFindElementByExactText(node.shadowRoot, text);
1332
+ if (found) return found;
1333
+ }
1334
+ }
1335
+ }
1336
+ return null;
1337
+ }
1338
+
1339
+ switch (data.method) {
1340
+ case 'initialize':
1341
+ PCIManager.initialize(data.params);
1342
+ break;
1343
+
1344
+ case 'setMarkup':
1345
+ PCIManager.setMarkup(data.params);
1346
+ break;
1347
+
1348
+ case 'setBoundTo':
1349
+ if (PCIManager.pciInstance) {
1350
+ PCIManager.applyBoundTo(data.params);
1351
+ } else {
1352
+ PCIManager.pendingBoundTo = data.params;
1353
+ }
1354
+ break;
1355
+
1356
+ case 'setProperties':
1357
+ PCIManager.setProperties(data.params);
1358
+ break;
1359
+
1360
+ case 'setStylesheets':
1361
+ PCIManager.setStylesheets(data.params);
1362
+ break;
1363
+
1364
+ case 'setState':
1365
+ if (PCIManager.pciInstance && typeof PCIManager.pciInstance.setState === 'function') {
1366
+ PCIManager.pciInstance.setState((data.params && data.params.state) || data.params);
1367
+ } else {
1368
+ PCIManager.pendingState = (data.params && data.params.state) || data.params;
1369
+ }
1370
+ break;
1371
+
1372
+ case 'getContent': {
1373
+ const messageId = data.params && data.params.messageId;
1374
+ const collectShadowHtml = root => {
1375
+ const parts = [];
1376
+ if (!root || !root.querySelectorAll) return parts;
1377
+ const nodes = root.querySelectorAll('*');
1378
+ for (const node of nodes) {
1379
+ if (node && node.shadowRoot) {
1380
+ parts.push(node.shadowRoot.innerHTML || '');
1381
+ parts.push(...collectShadowHtml(node.shadowRoot));
1382
+ }
1383
+ }
1384
+ return parts;
1385
+ };
1386
+ const shadowHtml = collectShadowHtml(document).join('\\n');
1387
+ window.parent.postMessage(
1388
+ {
1389
+ source: 'qti-pci-iframe',
1390
+ responseIdentifier: PCIManager.responseIdentifier,
1391
+ method: 'getContentResponse',
1392
+ messageId: messageId,
1393
+ content: (document.documentElement ? document.documentElement.outerHTML : '') + '\\n' + shadowHtml
1394
+ },
1395
+ '*'
1396
+ );
1397
+ break;
1398
+ }
1399
+
1400
+ case 'simulateClick': {
1401
+ const messageId = data.params && data.params.messageId;
1402
+ const x = data.params && data.params.x;
1403
+ const y = data.params && data.params.y;
1404
+ const el = typeof x === 'number' && typeof y === 'number' ? document.elementFromPoint(x, y) : null;
1405
+ const target = (el && el.closest && el.closest('.hitbox')) || document.querySelector('.hitbox') || el;
1406
+ if (target) {
1407
+ const evt = new MouseEvent('click', {
1408
+ bubbles: true,
1409
+ clientX: x,
1410
+ clientY: y,
1411
+ screenX: x,
1412
+ screenY: y,
1413
+ view: window
1414
+ });
1415
+ target.dispatchEvent(evt);
1416
+ }
1417
+ window.parent.postMessage(
1418
+ {
1419
+ source: 'qti-pci-iframe',
1420
+ responseIdentifier: PCIManager.responseIdentifier,
1421
+ method: 'clickResponse',
1422
+ messageId: messageId
1423
+ },
1424
+ '*'
1425
+ );
1426
+ break;
1427
+ }
1428
+
1429
+ case 'getBoundingRect': {
1430
+ const messageId = data.params && data.params.messageId;
1431
+ const selector = data.params && data.params.selector;
1432
+ const el = selector ? deepQuerySelector(document, selector) : null;
1433
+ const rect = el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
1434
+ window.parent.postMessage(
1435
+ {
1436
+ source: 'qti-pci-iframe',
1437
+ responseIdentifier: PCIManager.responseIdentifier,
1438
+ method: 'getBoundingRectResponse',
1439
+ messageId: messageId,
1440
+ rect: rect
1441
+ ? {
1442
+ left: rect.left,
1443
+ top: rect.top,
1444
+ width: rect.width,
1445
+ height: rect.height
1446
+ }
1447
+ : null
1448
+ },
1449
+ '*'
1450
+ );
1451
+ break;
1452
+ }
1453
+
1454
+ case 'clickOnSelector': {
1455
+ const messageId = data.params && data.params.messageId;
1456
+ const selector = data.params && data.params.selector;
1457
+ const el = selector ? deepQuerySelector(document, selector) : null;
1458
+ const success = !!el;
1459
+ if (el && typeof el.click === 'function') el.click();
1460
+ window.parent.postMessage(
1461
+ {
1462
+ source: 'qti-pci-iframe',
1463
+ responseIdentifier: PCIManager.responseIdentifier,
1464
+ method: 'clickSelectorResponse',
1465
+ messageId: messageId,
1466
+ success: success
1467
+ },
1468
+ '*'
1469
+ );
1470
+ break;
1471
+ }
1472
+
1473
+ case 'clickOnElementByText': {
1474
+ const messageId = data.params && data.params.messageId;
1475
+ const text = data.params && data.params.text;
1476
+ const target = text ? deepFindElementByExactText(document, text) : null;
1477
+ const success = !!target;
1478
+ if (target && typeof target.click === 'function') target.click();
1479
+ window.parent.postMessage(
1480
+ {
1481
+ source: 'qti-pci-iframe',
1482
+ responseIdentifier: PCIManager.responseIdentifier,
1483
+ method: 'clickTextResponse',
1484
+ messageId: messageId,
1485
+ success: success
1486
+ },
1487
+ '*'
1488
+ );
1489
+ break;
1490
+ }
1491
+
1492
+ case 'setValueElement': {
1493
+ const messageId = data.params && data.params.messageId;
1494
+ const selector = data.params && data.params.selector;
1495
+ const value = data.params && data.params.value;
1496
+ const el = selector ? deepQuerySelector(document, selector) : null;
1497
+ let success = false;
1498
+ if (el && 'value' in el) {
1499
+ try {
1500
+ el.value = value;
1501
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1502
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1503
+ success = true;
1504
+ } catch (e) {
1505
+ success = false;
1506
+ }
1507
+ }
1508
+ window.parent.postMessage(
1509
+ {
1510
+ source: 'qti-pci-iframe',
1511
+ responseIdentifier: PCIManager.responseIdentifier,
1512
+ method: 'setValueResponse',
1513
+ messageId: messageId,
1514
+ success: success
1515
+ },
1516
+ '*'
1517
+ );
1518
+ break;
1519
+ }
1520
+
1521
+ case 'console':
1522
+ window.parent.postMessage(
1523
+ {
1524
+ source: 'qti-pci-iframe',
1525
+ responseIdentifier: PCIManager.responseIdentifier,
1526
+ method: 'console',
1527
+ level: data.level,
1528
+ args: data.args
1529
+ },
1530
+ '*'
1531
+ );
1532
+ break;
1533
+ }
1534
+ });
1535
+
1536
+ let resizeTimeout;
1537
+ let previousHeight = 0;
1538
+ const notifyResize = () => {
1539
+ const container = document.getElementById('pci-container');
1540
+ const newHeight = container.scrollHeight + 100;
1541
+ if (newHeight !== previousHeight) {
1542
+ previousHeight = newHeight;
1543
+ clearTimeout(resizeTimeout);
1544
+ resizeTimeout = setTimeout(() => {
1545
+ window.parent.postMessage(
1546
+ {
1547
+ source: 'qti-pci-iframe',
1548
+ responseIdentifier: PCIManager.responseIdentifier,
1549
+ method: 'resize',
1550
+ height: newHeight,
1551
+ width: container.scrollWidth
1552
+ },
1553
+ '*'
1554
+ );
1555
+ }, 100); // Adjust debounce time as needed
1556
+ }
1557
+ };
1558
+
1559
+ function setupResizeObserver() {
1560
+ const container = document.getElementById('pci-container');
1561
+ if (!container || !(container instanceof Element)) {
1562
+ console.warn('ResizeObserver: document.container is not an Element');
1563
+ return;
1564
+ }
1565
+
1566
+ const resizeObserver = new ResizeObserver(() => {
1567
+ notifyResize();
1568
+ });
1569
+
1570
+ resizeObserver.observe(container);
1571
+ }
1572
+
1573
+ // Run setup once DOM is ready
1574
+ if (document.readyState === 'loading') {
1575
+ document.addEventListener('DOMContentLoaded', () => {
1576
+ notifyResize(); // initial resize
1577
+ setupResizeObserver();
1578
+ });
1579
+ } else {
1580
+ notifyResize();
1581
+ setupResizeObserver();
1582
+ }
1583
+
1584
+ window.addEventListener('load', () => {
1585
+ notifyResize();
1586
+ });
1587
+ let lastResponseStr = '';
1588
+ setInterval(() => {
1589
+ if (PCIManager.interactionChangedViaEvent) return;
1590
+ if (PCIManager.pciInstance && PCIManager.pciInstance.getResponse) {
1591
+ const response = PCIManager.pciInstance.getResponse();
1592
+ const valid =
1593
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.checkValidity === 'function'
1594
+ ? PCIManager.pciInstance.checkValidity()
1595
+ : undefined;
1596
+ const customValidity =
1597
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.getCustomValidity === 'function'
1598
+ ? PCIManager.pciInstance.getCustomValidity()
1599
+ : undefined;
1600
+ if (response === undefined) {
1601
+ // Don't emit an initial empty on load; only emit a clear if we previously had a value
1602
+ if (!PCIManager.hadResponse) return;
1603
+ PCIManager.hadResponse = false;
1604
+ PCIManager.lastResponseStr = null;
1605
+ const state =
1606
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function'
1607
+ ? PCIManager.pciInstance.getState()
1608
+ : null;
1609
+ window.parent.postMessage(
1610
+ {
1611
+ source: 'qti-pci-iframe',
1612
+ responseIdentifier: PCIManager.responseIdentifier,
1613
+ method: 'interactionChanged',
1614
+ params: { value: null, state: state, valid: valid, customValidity: customValidity }
1615
+ },
1616
+ '*'
1617
+ );
1618
+ return;
1619
+ }
1620
+
1621
+ const responseStr = JSON.stringify(response);
1622
+
1623
+ if (responseStr !== PCIManager.lastResponseStr) {
1624
+ PCIManager.lastResponseStr = responseStr;
1625
+ PCIManager.hadResponse = true;
1626
+ const state =
1627
+ PCIManager.pciInstance && typeof PCIManager.pciInstance.getState === 'function'
1628
+ ? PCIManager.pciInstance.getState()
1629
+ : null;
1630
+ window.parent.postMessage(
1631
+ {
1632
+ source: 'qti-pci-iframe',
1633
+ responseIdentifier: PCIManager.responseIdentifier,
1634
+ method: 'interactionChanged',
1635
+ params: { value: response, state: state, valid: valid, customValidity: customValidity }
1636
+ },
1637
+ '*'
1638
+ );
1639
+ }
1640
+ }
1641
+ }, 500); // Check every 500ms
1642
+ </script>
1643
+ </head>
1644
+ <body>
1645
+ <div id="pci-container"></div>
1646
+ </body>
1647
+ </html>`;
1648
+ }
1649
+ /**
1650
+ * Toggle the display of the correct response
1651
+ * @param responseVariable The response variable containing the correct response
1652
+ * @param show Whether to show or hide the correct response
1653
+ */
1654
+ toggleInternalCorrectResponse(show) {
1655
+ const responseVariable = this.responseVariable;
1656
+ // Store the correct response or clear it based on the show parameter
1657
+ this.correctResponse = show
1658
+ ? responseVariable?.correctResponse
1659
+ : responseVariable.cardinality === 'single'
1660
+ ? ''
1661
+ : [];
1662
+ // Get unique identifiers for this PCI's correct response elements
1663
+ const containerId = `correct-response-container-${this.responseIdentifier}`;
1664
+ // Check the parent element for any existing containers with this ID
1665
+ const existingContainers = this.parentElement?.querySelectorAll(`#${containerId}`);
1666
+ if (existingContainers) {
1667
+ existingContainers.forEach(existingContainer => {
1668
+ existingContainer.remove();
1669
+ });
1670
+ }
1671
+ // Handle the current interaction's state
1672
+ if (show) {
1673
+ // Disable the current interaction when showing correct response
1674
+ this.disable();
1675
+ }
1676
+ else {
1677
+ // Enable the current interaction when hiding correct response
1678
+ this.enable();
1679
+ return; // Exit early, nothing else to do
1680
+ }
1681
+ // If there's no correct response to show, exit
1682
+ if (!show || !responseVariable?.correctResponse) {
1683
+ return;
1684
+ }
1685
+ // Create a container for the correct response viewer
1686
+ const correctResponseContainer = document.createElement('div');
1687
+ correctResponseContainer.id = containerId;
1688
+ correctResponseContainer.className = 'pci-correct-response-container';
1689
+ correctResponseContainer.style.position = 'relative';
1690
+ correctResponseContainer.style.marginTop = '20px';
1691
+ correctResponseContainer.style.border = '2px solid green';
1692
+ correctResponseContainer.style.padding = '16px';
1693
+ correctResponseContainer.style.borderRadius = '4px';
1694
+ correctResponseContainer.style.backgroundColor = 'rgba(0, 128, 0, 0.05)';
1695
+ // Add a label for the correct response
1696
+ const label = document.createElement('div');
1697
+ label.textContent = 'Correct Response:';
1698
+ label.style.fontWeight = 'bold';
1699
+ label.style.marginBottom = '10px';
1700
+ label.style.color = 'green';
1701
+ correctResponseContainer.appendChild(label);
1702
+ // Instead of cloning, we'll create a new instance and copy necessary attributes
1703
+ const correctResponseViewer = document.createElement('qti-portable-custom-interaction');
1704
+ // Copy all attributes from the original PCI
1705
+ Array.from(this.attributes).forEach(attr => {
1706
+ if (attr.name !== 'id' && attr.name !== 'response-identifier') {
1707
+ correctResponseViewer.setAttribute(attr.name, attr.value);
1708
+ }
1709
+ });
1710
+ // Set a unique response identifier to avoid conflicts
1711
+ const originalResponseId = this.responseIdentifier;
1712
+ correctResponseViewer.responseIdentifier = `${originalResponseId}-correct`;
1713
+ // Copy any light DOM content from the original PCI
1714
+ // This includes markup and properties
1715
+ Array.from(this.children).forEach(child => {
1716
+ const clonedChild = child.cloneNode(true);
1717
+ correctResponseViewer.appendChild(clonedChild);
1718
+ });
1719
+ // Store the correct response value
1720
+ const correctResponseValue = responseVariable.correctResponse;
1721
+ // Ensure the correct-response viewer is initialized and then configured in the iframe
1722
+ const originalConnectedCallback = correctResponseViewer.connectedCallback;
1723
+ correctResponseViewer.connectedCallback = function () {
1724
+ originalConnectedCallback.call(this);
1725
+ const applyCorrectResponse = () => {
1726
+ const qtiVariableJSON = this.responseVariablesToQtiVariableJSON(correctResponseValue, responseVariable.cardinality, responseVariable.baseType);
1727
+ this.sendMessageToIframe('setBoundTo', {
1728
+ [originalResponseId]: qtiVariableJSON
1729
+ });
1730
+ this.sendMessageToIframe('setState', { state: 'review' });
1731
+ };
1732
+ if (this._iframeLoaded) {
1733
+ applyCorrectResponse();
1734
+ }
1735
+ else {
1736
+ this.addEventListener('qti-portable-custom-interaction-loaded', applyCorrectResponse, { once: true });
1737
+ }
1738
+ };
1739
+ // Make sure the viewer is not interactive
1740
+ correctResponseViewer.style.pointerEvents = 'none';
1741
+ // Add the correct response viewer to the container
1742
+ correctResponseContainer.appendChild(correctResponseViewer);
1743
+ // Append the container after this PCI
1744
+ this.after(correctResponseContainer);
1745
+ }
1746
+ /**
1747
+ * Method to disable the PCI for review mode
1748
+ * This can be used when showing the correct response
1749
+ */
1750
+ disable() {
1751
+ // First, store the current state of the PCI
1752
+ this.#previousState = {
1753
+ pointerEvents: this.style.pointerEvents,
1754
+ position: this.style.position
1755
+ };
1756
+ this.sendMessageToIframe('setState', { state: 'disabled' });
1757
+ // Add an overlay to prevent interaction if not already there
1758
+ const existingOverlay = this.querySelector('.pci-interaction-overlay');
1759
+ if (!existingOverlay) {
1760
+ const overlay = document.createElement('div');
1761
+ overlay.className = 'pci-interaction-overlay';
1762
+ overlay.style.position = 'absolute';
1763
+ overlay.style.top = '0';
1764
+ overlay.style.left = '0';
1765
+ overlay.style.width = '100%';
1766
+ overlay.style.height = '100%';
1767
+ overlay.style.backgroundColor = 'rgba(200, 200, 200, 0.3)';
1768
+ overlay.style.zIndex = '100';
1769
+ overlay.style.pointerEvents = 'all';
1770
+ overlay.style.cursor = 'not-allowed';
1771
+ // Make sure the container is relatively positioned
1772
+ if (getComputedStyle(this).position === 'static') {
1773
+ this.style.position = 'relative';
1774
+ }
1775
+ this.appendChild(overlay);
1776
+ }
1777
+ }
1778
+ /**
1779
+ * Method to enable the PCI for interactive mode
1780
+ */
1781
+ enable() {
1782
+ // Remove any overlays
1783
+ const overlay = this.querySelector('.pci-interaction-overlay');
1784
+ if (overlay) {
1785
+ overlay.remove();
1786
+ }
1787
+ // Restore previous state if available
1788
+ if (this.#previousState) {
1789
+ if (this.#previousState.pointerEvents) {
1790
+ this.style.pointerEvents = this.#previousState.pointerEvents;
1791
+ }
1792
+ if (this.#previousState.position) {
1793
+ this.style.position = this.#previousState.position;
1794
+ }
1795
+ this.#previousState = null;
1796
+ }
1797
+ this.sendMessageToIframe('setState', { state: 'interacting' });
1798
+ }
1799
+ // Add this property to store the previous state
1800
+ #previousState;
1801
+ render() {
1802
+ return html `
1803
+ <slot></slot>
1804
+ ${this._errorMessage
1805
+ ? html `<div style="color:red">
1806
+ <h1>Error</h1>
1807
+ ${this._errorMessage}
1808
+ </div>`
1809
+ : ''}
1810
+ `;
1811
+ }
1812
+ }
1813
+ __decorate([
1814
+ property({ type: String, attribute: 'module' })
1815
+ ], QtiPortableCustomInteraction.prototype, "module", void 0);
1816
+ __decorate([
1817
+ property({ type: String, attribute: 'custom-interaction-type-identifier' })
1818
+ ], QtiPortableCustomInteraction.prototype, "customInteractionTypeIdentifier", void 0);
1819
+ __decorate([
1820
+ property({ type: String, attribute: 'data-require-paths' })
1821
+ ], QtiPortableCustomInteraction.prototype, "requirePathsJson", void 0);
1822
+ __decorate([
1823
+ property({ type: String, attribute: 'data-require-shim' })
1824
+ ], QtiPortableCustomInteraction.prototype, "requireShimJson", void 0);
1825
+ __decorate([
1826
+ property({ type: String, attribute: 'data-require-js-url' })
1827
+ ], QtiPortableCustomInteraction.prototype, "requireJsUrl", void 0);
1828
+ __decorate([
1829
+ property({ type: String, attribute: 'data-base-url' })
1830
+ ], QtiPortableCustomInteraction.prototype, "baseUrl", void 0);
1831
+ __decorate([
1832
+ property({ type: Boolean, attribute: 'data-use-default-paths' })
1833
+ ], QtiPortableCustomInteraction.prototype, "useDefaultPaths", void 0);
1834
+ __decorate([
1835
+ state()
1836
+ ], QtiPortableCustomInteraction.prototype, "_errorMessage", void 0);
1837
+ __decorate([
1838
+ consume({ context: itemContext, subscribe: true }),
1839
+ state()
1840
+ ], QtiPortableCustomInteraction.prototype, "context", void 0);
1841
+ __decorate([
1842
+ consume({ context: qtiContext, subscribe: true }),
1843
+ state()
1844
+ ], QtiPortableCustomInteraction.prototype, "qtiContext", void 0);
1845
+ __decorate([
1846
+ state()
1847
+ ], QtiPortableCustomInteraction.prototype, "response", void 0);
1848
+ //# sourceMappingURL=qti-portable-custom-interaction.js.map