@citolab/qti-components 7.13.0 → 7.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/cdn/index.global.js +1 -1
  2. package/cdn/index.js +103 -103
  3. package/custom-elements.json +1128 -315
  4. package/dist/chunks/{chunk-IHE5M7QU.js → chunk-CNLDFJAA.js} +4 -4
  5. package/dist/chunks/chunk-CNLDFJAA.js.map +1 -0
  6. package/dist/chunks/{chunk-WFUXZ4UT.js → chunk-LJ4KAG72.js} +26 -36
  7. package/dist/chunks/chunk-LJ4KAG72.js.map +1 -0
  8. package/dist/chunks/{chunk-TFNRSY74.js → chunk-SJGUMIKK.js} +24 -19
  9. package/dist/chunks/chunk-SJGUMIKK.js.map +1 -0
  10. package/dist/chunks/{chunk-XI7S3HP2.js → chunk-XD7DR5YG.js} +363 -337
  11. package/dist/chunks/chunk-XD7DR5YG.js.map +1 -0
  12. package/dist/exports/qti-test.d.ts +2 -2
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +47 -36
  15. package/dist/index.js.map +1 -1
  16. package/dist/qti-components/index.d.ts +10 -9
  17. package/dist/qti-components/index.js +4 -2
  18. package/dist/qti-components-jsx.d.ts +15 -15
  19. package/dist/qti-item/index.js +2 -2
  20. package/dist/qti-loader/index.js +2 -2
  21. package/dist/qti-loader/index.js.map +1 -1
  22. package/dist/qti-test/index.d.ts +3 -3
  23. package/dist/qti-test/index.js +3 -3
  24. package/dist/{qti-test-DEJqAn7G.d.ts → qti-test-Db7oNIWY.d.ts} +1 -1
  25. package/dist/{qti-transform-test-DkSRdVBF.d.ts → qti-transform-test-Bz9A3hmD.d.ts} +2 -5
  26. package/dist/qti-transformers/index.d.ts +3 -3
  27. package/dist/qti-transformers/index.js +1 -1
  28. package/dist/vscode.html-custom-data.json +4 -21
  29. package/package.json +10 -1
  30. package/dist/chunks/chunk-IHE5M7QU.js.map +0 -1
  31. package/dist/chunks/chunk-TFNRSY74.js.map +0 -1
  32. package/dist/chunks/chunk-WFUXZ4UT.js.map +0 -1
  33. package/dist/chunks/chunk-XI7S3HP2.js.map +0 -1
@@ -1,16 +1,16 @@
1
- import {
2
- item_default
3
- } from "./chunk-PT5ASWGQ.js";
4
1
  import {
5
2
  QtiModalFeedback
6
3
  } from "./chunk-YD7FVKDP.js";
4
+ import {
5
+ item_default
6
+ } from "./chunk-PT5ASWGQ.js";
7
7
  import {
8
8
  watch
9
9
  } from "./chunk-ELDMXTUQ.js";
10
10
  import {
11
11
  qtiTransformItem,
12
12
  qtiTransformTest
13
- } from "./chunk-WFUXZ4UT.js";
13
+ } from "./chunk-LJ4KAG72.js";
14
14
  import {
15
15
  qtiContext
16
16
  } from "./chunk-H6KHXSIO.js";
@@ -38,14 +38,6 @@ import { customElement } from "lit/decorators.js";
38
38
 
39
39
  // src/lib/qti-test/core/mixins/test-navigation.mixin.ts
40
40
  import { property } from "lit/decorators.js";
41
- var NavigationErrorType = /* @__PURE__ */ ((NavigationErrorType2) => {
42
- NavigationErrorType2["ITEM_NOT_FOUND"] = "item-not-found";
43
- NavigationErrorType2["SECTION_NOT_FOUND"] = "section-not-found";
44
- NavigationErrorType2["LOAD_ERROR"] = "load-error";
45
- NavigationErrorType2["NETWORK_ERROR"] = "network-error";
46
- NavigationErrorType2["TIMEOUT_ERROR"] = "timeout-error";
47
- return NavigationErrorType2;
48
- })(NavigationErrorType || {});
49
41
  var TestNavigationMixin = (superClass) => {
50
42
  class TestNavigationClass extends superClass {
51
43
  constructor(...args) {
@@ -53,378 +45,423 @@ var TestNavigationMixin = (superClass) => {
53
45
  this.navigate = null;
54
46
  this.cacheTransform = false;
55
47
  this.requestTimeout = 3e4;
56
- this.showLoadingIndicators = true;
57
48
  this.postLoadTransformCallback = null;
58
49
  this.postLoadTestTransformCallback = null;
59
- this._navigationInProgress = false;
60
- this._activeRequests = [];
61
- this._lastError = null;
62
- this._lastNavigationRequestId = null;
63
- this._targetNavigation = null;
64
- this.addEventListener(
65
- "qti-request-navigation",
66
- async ({ detail }) => {
67
- if (!detail?.id) return;
68
- const navigationRequestId = `nav_${Date.now()}_${Math.random()}`;
69
- this._lastNavigationRequestId = navigationRequestId;
70
- try {
71
- this._navigationInProgress = true;
72
- this._lastError = null;
73
- this._dispatchStatusEvent({ loading: true, type: detail.type, id: detail.id });
74
- this._cancelActiveRequests();
75
- this._targetNavigation = { type: detail.type, id: detail.id };
76
- if (detail.type === "item") {
77
- await this._navigateToItem(detail.id);
78
- } else if (detail.type === "section") {
79
- await this._navigateToSection(detail.id);
80
- }
81
- if (this._lastNavigationRequestId !== navigationRequestId) {
82
- console.log("Navigation was superseded by a newer request");
83
- return;
84
- }
85
- } catch (error) {
86
- if (this._lastNavigationRequestId === navigationRequestId) {
87
- const navError = this._normalizeError(error, detail.type, detail.id);
88
- this._lastError = navError;
89
- this._dispatchErrorEvent(navError);
90
- console.error(`Navigation error (${navError.type}):`, navError.message, navError.details);
91
- }
92
- } finally {
93
- if (this._lastNavigationRequestId === navigationRequestId) {
94
- this._navigationInProgress = false;
95
- this._dispatchStatusEvent({ loading: false, type: detail.type, id: detail.id });
96
- }
97
- }
98
- }
99
- );
100
- this.addEventListener("qti-assessment-test-connected", (e) => {
101
- this._testElement = e.detail;
102
- this._initializeNavigation();
103
- });
50
+ // Navigation state tracking
51
+ this._activeController = null;
52
+ this._loadResults = [];
53
+ // Simple loading progress tracking
54
+ this._loadingState = {
55
+ expectedItems: 0,
56
+ connectedItems: 0,
57
+ expectedStimulus: 0,
58
+ loadedStimulus: 0,
59
+ isComplete: false
60
+ };
61
+ // Track loaded/loading stimulus hrefs to prevent duplicates
62
+ this._loadedStimulusHrefs = /* @__PURE__ */ new Set();
63
+ this._loadingStimulusHrefs = /* @__PURE__ */ new Set();
64
+ this._bindEventHandlers();
104
65
  }
66
+ // ===========================================
67
+ // PUBLIC API
68
+ // ===========================================
105
69
  /**
106
- * Initialize navigation when test is first connected
70
+ * Navigate to a specific item or section
71
+ * @param type - Navigation type ('item' or 'section')
72
+ * @param id - Target identifier (optional, falls back to first available)
107
73
  */
108
- _initializeNavigation() {
109
- let id;
110
- if (this.navigate === "section") {
111
- id = this._testElement.querySelector("qti-assessment-section")?.identifier;
112
- }
113
- if (this.navigate === "item") {
114
- id = this.sessionContext.navItemRefId ?? this._testElement.querySelector("qti-assessment-item-ref")?.identifier;
115
- }
116
- if (id) {
74
+ navigateTo(type, id) {
75
+ const targetId = id || this._getDefaultNavigationId(type);
76
+ if (targetId) {
117
77
  this.dispatchEvent(
118
78
  new CustomEvent("qti-request-navigation", {
119
- detail: { type: this.navigate === "section" ? "section" : "item", id },
79
+ detail: { type, id: targetId },
120
80
  bubbles: true,
121
81
  composed: true
122
82
  })
123
83
  );
124
84
  }
125
85
  }
126
- navigateTo(type, id) {
127
- if (!id) {
128
- if (type === "section") {
129
- id = this._testElement?.querySelector("qti-assessment-section")?.identifier;
130
- }
131
- if (type === "item") {
132
- id = this._testElement?.querySelector("qti-assessment-item-ref")?.identifier;
86
+ // ===========================================
87
+ // EVENT HANDLER SETUP
88
+ // ===========================================
89
+ _bindEventHandlers() {
90
+ this.addEventListener("qti-request-navigation", this._handleNavigationRequest.bind(this));
91
+ this.addEventListener("qti-assessment-test-connected", this._handleTestConnected.bind(this));
92
+ this.addEventListener("qti-assessment-item-connected", this._handleItemConnected.bind(this));
93
+ this.addEventListener("qti-assessment-stimulus-ref-connected", this._handleStimulusRefConnected.bind(this));
94
+ }
95
+ _handleTestConnected(e) {
96
+ this._testElement = e.detail;
97
+ this._initializeNavigation();
98
+ }
99
+ /**
100
+ * Handle item connection events - track connected items and discover stimulus references
101
+ */
102
+ _handleItemConnected(e) {
103
+ const itemRef = e.detail;
104
+ this._loadingState.connectedItems++;
105
+ const stimulusRefs = itemRef.querySelectorAll("qti-assessment-stimulus-ref");
106
+ this._loadingState.expectedStimulus += stimulusRefs.length;
107
+ this._checkLoadingComplete();
108
+ }
109
+ /**
110
+ * Handle stimulus reference connection events with duplicate prevention
111
+ */
112
+ async _handleStimulusRefConnected(e) {
113
+ e.preventDefault();
114
+ const { element, item } = e;
115
+ console.info("Stimulus ref connected:", {
116
+ identifier: element.identifier,
117
+ href: element.href,
118
+ item: item.identifier
119
+ });
120
+ if (this._loadedStimulusHrefs.has(element.href)) {
121
+ console.info("Stimulus already loaded, skipping:", element.href);
122
+ this._loadingState.loadedStimulus++;
123
+ this._checkLoadingComplete();
124
+ return;
125
+ }
126
+ if (this._loadingStimulusHrefs.has(element.href)) {
127
+ console.info("Stimulus already loading, skipping duplicate:", element.href);
128
+ this._loadingState.loadedStimulus++;
129
+ this._checkLoadingComplete();
130
+ return;
131
+ }
132
+ this._loadingStimulusHrefs.add(element.href);
133
+ console.info("Starting stimulus load:", element.href);
134
+ try {
135
+ await this._loadStimulusRef(element, item);
136
+ this._loadedStimulusHrefs.add(element.href);
137
+ this._loadingState.loadedStimulus++;
138
+ console.info("Stimulus loaded successfully:", element.href);
139
+ this._checkLoadingComplete();
140
+ } catch (error) {
141
+ if (error.name !== "AbortError") {
142
+ console.warn(`Failed to load stimulus ${element.identifier}:`, error);
133
143
  }
144
+ this._loadingState.loadedStimulus++;
145
+ this._checkLoadingComplete();
146
+ } finally {
147
+ this._loadingStimulusHrefs.delete(element.href);
148
+ }
149
+ }
150
+ // ===========================================
151
+ // NAVIGATION FLOW
152
+ // ===========================================
153
+ _getDefaultNavigationId(type) {
154
+ if (type === "section") {
155
+ return this._testElement?.querySelector("qti-assessment-section")?.identifier;
156
+ }
157
+ return this.sessionContext?.navItemRefId ?? this._testElement?.querySelector("qti-assessment-item-ref")?.identifier;
158
+ }
159
+ _initializeNavigation() {
160
+ if (!this.navigate) return;
161
+ const id = this._getDefaultNavigationId(this.navigate);
162
+ if (id) {
163
+ this.navigateTo(this.navigate, id);
134
164
  }
135
- this.dispatchEvent(
136
- new CustomEvent("qti-request-navigation", {
137
- detail: { type, id },
138
- bubbles: true,
139
- composed: true
140
- })
141
- );
142
165
  }
143
166
  /**
144
- * Navigates to a specific item
167
+ * Main navigation request handler with proper lifecycle management
145
168
  */
146
- async _navigateToItem(itemId) {
147
- const itemRefEl = this._testElement?.querySelector(
148
- `qti-assessment-item-ref[identifier="${itemId}"]`
149
- );
150
- if (!itemRefEl) {
151
- throw {
152
- type: "item-not-found" /* ITEM_NOT_FOUND */,
153
- message: `Item with identifier "${itemId}" not found.`,
154
- itemId
155
- };
156
- }
157
- const navPartId = itemRefEl.closest("qti-test-part")?.identifier;
158
- const navSectionId = itemRefEl.closest("qti-assessment-section")?.identifier;
159
- this.sessionContext = {
160
- ...this.sessionContext,
161
- navPartId,
162
- navSectionId,
163
- navItemRefId: itemId,
164
- navItemLoading: true
165
- };
169
+ async _handleNavigationRequest({ detail }) {
170
+ if (!detail?.id) return;
171
+ this._cancelPreviousNavigation();
166
172
  try {
167
- await this._loadItems([itemId]);
173
+ this._dispatchLoadingStarted(detail.type, detail.id);
174
+ this._activeController = new AbortController();
175
+ await this._executeNavigation(detail.type, detail.id);
176
+ } catch (error) {
177
+ this._handleNavigationError(error, detail.type, detail.id);
168
178
  } finally {
169
- this.sessionContext = {
170
- ...this.sessionContext,
171
- navItemLoading: false
172
- };
179
+ this._activeController = null;
180
+ this._dispatchLoadingEnded(detail.type, detail.id);
181
+ }
182
+ }
183
+ _handleNavigationError(error, type, id) {
184
+ if (error.name !== "AbortError") {
185
+ this._dispatchError(this._createNavigationError(error, type, id));
186
+ }
187
+ }
188
+ async _executeNavigation(type, id) {
189
+ if (type === "item") {
190
+ await this._navigateToItem(id);
191
+ } else {
192
+ await this._navigateToSection(id);
173
193
  }
174
194
  }
175
195
  /**
176
- * Navigates to a specific section
196
+ * Navigate to specific item with simple state tracking
197
+ */
198
+ async _navigateToItem(itemId) {
199
+ const itemRef = this._findItemRef(itemId);
200
+ this._updateSessionContext(itemRef, itemId);
201
+ this._resetLoadingState();
202
+ this._loadingState.expectedItems = 1;
203
+ await this._loadItems([itemId]);
204
+ await this._waitForLoadingComplete();
205
+ this._dispatchTestLoaded(this._loadResults);
206
+ }
207
+ /**
208
+ * Navigate to section with simple state tracking
177
209
  */
178
210
  async _navigateToSection(sectionId) {
179
- const sectionRefEl = this._testElement?.querySelector(
180
- `qti-assessment-section[identifier="${sectionId}"]`
181
- );
182
- if (!sectionRefEl) {
183
- throw {
184
- type: "section-not-found" /* SECTION_NOT_FOUND */,
185
- message: `Section with identifier "${sectionId}" not found.`,
186
- sectionId
187
- };
188
- }
189
- const navPartId = sectionRefEl.closest("qti-test-part")?.identifier;
211
+ const sectionEl = this._findSection(sectionId);
212
+ const navPartId = sectionEl?.closest("qti-test-part")?.identifier;
190
213
  this.sessionContext = {
191
214
  ...this.sessionContext,
192
215
  navPartId,
193
216
  navSectionId: sectionId,
194
- navItemRefId: null,
195
- navItemLoading: true
217
+ navItemRefId: null
196
218
  };
197
- try {
198
- const itemIds = this._getSectionItemIds(sectionId);
199
- await this._loadItems(itemIds);
200
- } finally {
201
- this.sessionContext = {
202
- ...this.sessionContext,
203
- navItemLoading: false
204
- };
205
- }
219
+ const itemIds = this._getSectionItemIds(sectionId);
220
+ this._resetLoadingState();
221
+ this._loadingState.expectedItems = itemIds.length;
222
+ await this._loadItems(itemIds);
223
+ await this._waitForLoadingComplete();
224
+ this._dispatchTestLoaded(this._loadResults);
206
225
  }
226
+ // ===========================================
227
+ // LOADING STATE MANAGEMENT
228
+ // ===========================================
207
229
  /**
208
- * Normalize different error types into a consistent NavigationError format
230
+ * Reset loading state for new navigation
209
231
  */
210
- _normalizeError(error, navigationType, id) {
211
- if (error && error.type && Object.values(NavigationErrorType).includes(error.type)) {
212
- return error;
213
- }
214
- if (error instanceof DOMException && error.name === "AbortError") {
215
- return {
216
- type: "network-error" /* NETWORK_ERROR */,
217
- message: "Navigation was cancelled because a new navigation was requested.",
218
- details: error
219
- };
220
- }
221
- if (error.name === "TimeoutError" || error.message && error.message.includes("timeout")) {
222
- return {
223
- type: "timeout-error" /* TIMEOUT_ERROR */,
224
- message: "Request timed out. Please check your network connection.",
225
- details: error
226
- };
232
+ _resetLoadingState() {
233
+ this._loadingState = {
234
+ expectedItems: 0,
235
+ connectedItems: 0,
236
+ expectedStimulus: 0,
237
+ loadedStimulus: 0,
238
+ isComplete: false
239
+ };
240
+ this._loadedStimulusHrefs.clear();
241
+ this._loadingStimulusHrefs.clear();
242
+ }
243
+ /**
244
+ * Check if loading is complete and dispatch events accordingly
245
+ */
246
+ _checkLoadingComplete() {
247
+ const allItemsConnected = this._loadingState.connectedItems >= this._loadingState.expectedItems;
248
+ const allStimulusLoaded = this._loadingState.loadedStimulus >= this._loadingState.expectedStimulus;
249
+ if (allItemsConnected && allStimulusLoaded && !this._loadingState.isComplete) {
250
+ this._loadingState.isComplete = true;
251
+ console.info("Loading complete:", this._loadingState);
227
252
  }
228
- if (error instanceof TypeError && error.message.includes("network")) {
229
- return {
230
- type: "network-error" /* NETWORK_ERROR */,
231
- message: "A network error occurred. Please check your connection.",
232
- details: error
233
- };
253
+ }
254
+ /**
255
+ * Wait for loading to complete with simple polling
256
+ */
257
+ async _waitForLoadingComplete() {
258
+ while (!this._loadingState.isComplete) {
259
+ await new Promise((resolve) => setTimeout(resolve, 10));
234
260
  }
235
- return {
236
- type: "load-error" /* LOAD_ERROR */,
237
- message: `Failed to load ${navigationType}: ${id}`,
238
- details: error,
239
- itemId: navigationType === "item" ? id : void 0,
240
- sectionId: navigationType === "section" ? id : void 0
241
- };
242
261
  }
243
262
  /**
244
- * Dispatch error event to notify the UI
263
+ * Get current loading progress for external consumption
245
264
  */
246
- _dispatchErrorEvent(error) {
247
- this.dispatchEvent(
248
- new CustomEvent("qti-navigation-error", {
249
- detail: error,
250
- bubbles: true,
251
- composed: true
252
- })
253
- );
265
+ getLoadingProgress() {
266
+ return { ...this._loadingState };
254
267
  }
255
268
  /**
256
- * Dispatch status event to indicate loading state
269
+ * Load stimulus reference with simple tracking
257
270
  */
258
- _dispatchStatusEvent(status) {
259
- if (this.showLoadingIndicators) {
260
- this.dispatchEvent(
261
- new CustomEvent("qti-navigation-status", {
262
- detail: status,
263
- bubbles: true,
264
- composed: true
265
- })
266
- );
271
+ async _loadStimulusRef(element, item) {
272
+ console.info("Loading stimulus:", element.href);
273
+ const stimulus = await this._loadStimulus(element.href);
274
+ console.info("Stimulus loaded, applying content:", stimulus ? "has content" : "no content");
275
+ this._applyStimulusContent(stimulus, element, item);
276
+ }
277
+ _applyStimulusContent(stimulus, element, item) {
278
+ if (!stimulus) {
279
+ console.warn("No stimulus content to apply");
280
+ return;
281
+ }
282
+ const elements = stimulus.querySelectorAll("qti-stimulus-body, qti-stylesheet");
283
+ console.info(`Found ${elements.length} stimulus elements to apply for ${element.identifier}`);
284
+ if (elements.length === 0) {
285
+ console.warn("No qti-stimulus-body or qti-stylesheet elements found in stimulus");
286
+ return;
287
+ }
288
+ let targets = [];
289
+ const specificTarget = document.querySelector(
290
+ `qti-assessment-item[identifier="${item.identifier}"] [data-stimulus-idref="${element.identifier}"]`
291
+ );
292
+ if (specificTarget) {
293
+ targets.push(specificTarget);
294
+ console.info("Found specific target:", specificTarget);
295
+ } else {
296
+ const allTargetsWithId = Array.from(this.querySelectorAll(`[data-stimulus-idref="${element.identifier}"]`));
297
+ if (allTargetsWithId.length > 0) {
298
+ targets = allTargetsWithId;
299
+ console.info("Found targets by identifier:", allTargetsWithId.length);
300
+ } else {
301
+ const allStimulusTargets = Array.from(this.querySelectorAll("[data-stimulus-idref]"));
302
+ targets = allStimulusTargets;
303
+ console.info("Using fallback targets:", allStimulusTargets.length);
304
+ }
305
+ }
306
+ if (targets.length === 0) {
307
+ console.warn("No targets found for stimulus content");
308
+ return;
267
309
  }
310
+ targets.forEach((target, index) => {
311
+ target.innerHTML = "";
312
+ const clonedElements = Array.from(elements).map((el) => el.cloneNode(true));
313
+ target.append(...clonedElements);
314
+ console.info(`Applied stimulus content to target ${index + 1}/${targets.length}`);
315
+ });
268
316
  }
317
+ // ===========================================
318
+ // LOADING INFRASTRUCTURE
319
+ // ===========================================
269
320
  /**
270
- * Cancels all active HTTP requests
321
+ * Cancel previous navigation and clean up all state
271
322
  */
272
- _cancelActiveRequests() {
273
- if (this._activeRequests.length > 0) {
274
- console.info(`Cancelling ${this._activeRequests.length} pending requests`);
275
- this._activeRequests.forEach((request) => {
276
- if (request && request.readyState !== 4) {
277
- request.abort();
278
- }
279
- });
280
- this._activeRequests = [];
323
+ _cancelPreviousNavigation() {
324
+ if (this._activeController) {
325
+ this._activeController.abort();
326
+ console.info("Previous navigation request cancelled");
281
327
  }
328
+ this._clearNavigationState();
329
+ }
330
+ _clearNavigationState() {
331
+ this._clearLoadedItems();
332
+ this._resetLoadingState();
333
+ this._loadResults = [];
282
334
  }
283
335
  /**
284
- * Load items with improved error handling and timeout
336
+ * Load items with simple tracking
285
337
  */
286
338
  async _loadItems(itemIds) {
287
339
  if (!this._testElement || itemIds.length === 0) return;
288
- const itemRefEls = itemIds.map(
289
- (id) => this._testElement.querySelector(`qti-assessment-item-ref[identifier="${id}"]`)
290
- );
291
- const missingItems = itemRefEls.reduce((missing, el, index) => {
292
- if (!el) missing.push(itemIds[index]);
293
- return missing;
294
- }, []);
295
- if (missingItems.length > 0) {
296
- const error = {
297
- type: "item-not-found" /* ITEM_NOT_FOUND */,
298
- message: `One or more items not found: ${missingItems.join(", ")}`,
299
- details: { missingItems }
300
- };
301
- throw error;
302
- }
340
+ const itemRefs = itemIds.map((id) => this._findItemRef(id));
303
341
  this._clearLoadedItems();
304
- const itemLoadPromises = itemRefEls.map(async (itemRef) => {
305
- if (!itemRef) return null;
306
- const timeoutPromise = new Promise((_, reject) => {
307
- setTimeout(() => {
308
- reject({
309
- name: "TimeoutError",
310
- message: `Request for item ${itemRef.identifier} timed out after ${this.requestTimeout}ms`
311
- });
312
- }, this.requestTimeout);
313
- });
314
- try {
315
- const { promise, request } = qtiTransformItem(this.cacheTransform).load(itemRef.href);
316
- if (request instanceof XMLHttpRequest) {
317
- this._activeRequests.push(request);
318
- }
319
- const loadedTransformer = await Promise.race([promise, timeoutPromise]);
320
- let finalTransformer = loadedTransformer;
321
- if (this.postLoadTransformCallback) {
322
- finalTransformer = await this.postLoadTransformCallback(loadedTransformer, itemRef);
323
- }
324
- return {
325
- itemRef,
326
- doc: finalTransformer.htmlDoc(),
327
- request
328
- };
329
- } catch (error) {
330
- if (error instanceof DOMException && error.name === "AbortError" || error && error.name === "TimeoutError") {
331
- console.log(
332
- `Request for item ${itemRef.identifier} was ${error.name === "TimeoutError" ? "timed out" : "aborted"}`
333
- );
334
- return null;
335
- }
336
- error.itemId = itemRef.identifier;
337
- throw error;
338
- }
342
+ this._clearStimulusRef();
343
+ const results = await Promise.all(itemRefs.map((ref) => this._loadSingleItem(ref)));
344
+ const validResults = results.filter(Boolean);
345
+ validResults.forEach(({ itemRef, doc }) => {
346
+ if (itemRef && doc) itemRef.xmlDoc = doc;
339
347
  });
348
+ this._loadResults = validResults;
349
+ }
350
+ async _loadSingleItem(itemRef) {
340
351
  try {
341
- const results = await Promise.all(itemLoadPromises);
342
- const validResults = results.filter((result) => result !== null);
343
- validResults.forEach(({ itemRef, doc }) => {
344
- if (itemRef && doc) itemRef.xmlDoc = doc;
345
- });
346
- this._activeRequests = [];
347
- requestAnimationFrame(() => {
348
- this.dispatchEvent(
349
- new CustomEvent("qti-test-loaded", {
350
- detail: validResults.map(({ itemRef }) => ({
351
- identifier: itemRef?.identifier,
352
- element: itemRef
353
- })),
354
- bubbles: true,
355
- composed: true
356
- })
357
- );
358
- });
359
- if (validResults.length === 0 && itemIds.length > 0) {
360
- throw {
361
- type: "load-error" /* LOAD_ERROR */,
362
- message: "All item requests failed to load",
363
- details: { itemIds }
364
- };
352
+ let transformer = await qtiTransformItem(this.cacheTransform).load(
353
+ itemRef.href,
354
+ this._activeController?.signal
355
+ );
356
+ if (this.postLoadTransformCallback) {
357
+ transformer = await this.postLoadTransformCallback(transformer, itemRef);
365
358
  }
366
- return validResults;
359
+ return { itemRef, doc: transformer.htmlDoc() };
367
360
  } catch (error) {
368
- console.error("Error loading items:", error);
369
- throw error;
361
+ if (error.name === "AbortError") {
362
+ console.info(`Item load for ${itemRef.identifier} was aborted`);
363
+ throw error;
364
+ }
365
+ console.warn(`Failed to load item ${itemRef.identifier}:`, error);
366
+ return null;
370
367
  }
371
368
  }
372
- /**
373
- * Gets all item IDs in a section
374
- */
375
- _getSectionItemIds(navSectionId) {
376
- const sectionRefEl = this._testElement?.querySelector(
377
- `qti-assessment-section[identifier="${navSectionId}"]`
369
+ async _loadStimulus(href) {
370
+ const transformer = await qtiTransformItem().load(href, this._activeController?.signal);
371
+ return transformer.htmlDoc();
372
+ }
373
+ // ===========================================
374
+ // UTILITIES
375
+ // ===========================================
376
+ _findItemRef(itemId) {
377
+ const itemRef = this._testElement?.querySelector(
378
+ `qti-assessment-item-ref[identifier="${itemId}"]`
378
379
  );
379
- if (!sectionRefEl) {
380
- throw {
381
- type: "section-not-found" /* SECTION_NOT_FOUND */,
382
- message: `Section with identifier "${navSectionId}" not found.`,
383
- sectionId: navSectionId
384
- };
380
+ if (!itemRef) {
381
+ throw new Error(`Item with identifier "${itemId}" not found`);
385
382
  }
383
+ return itemRef;
384
+ }
385
+ _findSection(sectionId) {
386
+ return this._testElement?.querySelector(`qti-assessment-section[identifier="${sectionId}"]`) || null;
387
+ }
388
+ _updateSessionContext(itemRef, itemId) {
389
+ const navPartId = itemRef.closest("qti-test-part")?.identifier;
390
+ const navSectionId = itemRef.closest("qti-assessment-section")?.identifier;
391
+ this.sessionContext = {
392
+ ...this.sessionContext,
393
+ navPartId,
394
+ navSectionId,
395
+ navItemRefId: itemId
396
+ };
397
+ }
398
+ _getSectionItemIds(sectionId) {
386
399
  return Array.from(
387
400
  this._testElement.querySelectorAll(
388
- `qti-assessment-section[identifier="${navSectionId}"] > qti-assessment-item-ref`
401
+ `qti-assessment-section[identifier="${sectionId}"] > qti-assessment-item-ref`
389
402
  )
390
- ).map((itemRef) => itemRef.identifier);
403
+ ).map((ref) => ref.identifier).filter(Boolean);
391
404
  }
392
- /**
393
- * Clears all loaded items
394
- */
395
405
  _clearLoadedItems() {
396
- const itemRefEls = this._testElement?.querySelectorAll(
397
- `qti-assessment-test qti-assessment-item-ref`
398
- );
399
- Array.from(itemRefEls || []).forEach((itemElement) => {
400
- itemElement.xmlDoc = null;
406
+ const itemRefs = this._testElement?.querySelectorAll("qti-assessment-item-ref");
407
+ Array.from(itemRefs || []).forEach((element) => {
408
+ element.xmlDoc = null;
401
409
  });
402
410
  }
403
- /**
404
- * Retry the last failed navigation
405
- */
406
- retryNavigation() {
407
- if (this._lastError) {
408
- const type = this._lastError.itemId ? "item" : "section";
409
- const id = this._lastError.itemId || this._lastError.sectionId;
410
- if (id) {
411
- this.dispatchEvent(
412
- new CustomEvent("qti-request-navigation", {
413
- detail: { type, id },
414
- bubbles: true,
415
- composed: true
416
- })
417
- );
418
- }
419
- } else if (this._targetNavigation) {
411
+ _clearStimulusRef() {
412
+ this.querySelectorAll("[data-stimulus-idref]").forEach((el) => el.innerHTML = "");
413
+ }
414
+ _createNavigationError(error, type, id) {
415
+ return {
416
+ message: error.message || `Failed to load ${type}: ${id}`,
417
+ details: error,
418
+ itemId: type === "item" ? id : void 0,
419
+ sectionId: type === "section" ? id : void 0
420
+ };
421
+ }
422
+ // ===========================================
423
+ // EVENT DISPATCHING
424
+ // ===========================================
425
+ _dispatchLoadingStarted(type, id) {
426
+ this.dispatchEvent(
427
+ new CustomEvent("qti-navigation-loading-started", {
428
+ detail: { type, id },
429
+ bubbles: true,
430
+ composed: true
431
+ })
432
+ );
433
+ }
434
+ _dispatchLoadingEnded(type, id) {
435
+ this.dispatchEvent(
436
+ new CustomEvent("qti-navigation-loading-ended", {
437
+ detail: { type, id },
438
+ bubbles: true,
439
+ composed: true
440
+ })
441
+ );
442
+ }
443
+ _dispatchError(error) {
444
+ this.dispatchEvent(
445
+ new CustomEvent("qti-navigation-error", {
446
+ detail: error,
447
+ bubbles: true,
448
+ composed: true
449
+ })
450
+ );
451
+ }
452
+ _dispatchTestLoaded(results) {
453
+ requestAnimationFrame(() => {
420
454
  this.dispatchEvent(
421
- new CustomEvent("qti-request-navigation", {
422
- detail: this._targetNavigation,
455
+ new CustomEvent("qti-test-loaded", {
456
+ detail: results.map(({ itemRef }) => ({
457
+ identifier: itemRef?.identifier,
458
+ element: itemRef
459
+ })),
423
460
  bubbles: true,
424
461
  composed: true
425
462
  })
426
463
  );
427
- }
464
+ });
428
465
  }
429
466
  }
430
467
  __decorateClass([
@@ -437,18 +474,10 @@ var TestNavigationMixin = (superClass) => {
437
474
  property({ type: Number })
438
475
  ], TestNavigationClass.prototype, "requestTimeout", 2);
439
476
  __decorateClass([
440
- property({ type: Boolean })
441
- ], TestNavigationClass.prototype, "showLoadingIndicators", 2);
442
- __decorateClass([
443
- property({ type: Function })
477
+ property({ attribute: false })
444
478
  ], TestNavigationClass.prototype, "postLoadTransformCallback", 2);
445
479
  __decorateClass([
446
- property({
447
- type: Function,
448
- hasChanged: (newVal, oldVal) => {
449
- return newVal !== oldVal;
450
- }
451
- })
480
+ property({ attribute: false })
452
481
  ], TestNavigationClass.prototype, "postLoadTestTransformCallback", 2);
453
482
  return TestNavigationClass;
454
483
  };
@@ -704,7 +733,6 @@ QtiTest = __decorateClass([
704
733
  // src/lib/qti-test/core/qti-assessment-test/qti-assessment-item-ref.ts
705
734
  import { LitElement as LitElement2 } from "lit";
706
735
  import { property as property3 } from "lit/decorators.js";
707
- import { prepareTemplate } from "@heximal/templates";
708
736
  var stringToBooleanConverter = {
709
737
  fromAttribute(value) {
710
738
  return value === "true";
@@ -729,8 +757,6 @@ var QtiAssessmentItemRef = class extends LitElement2 {
729
757
  }
730
758
  async connectedCallback() {
731
759
  super.connectedCallback();
732
- const templateElement = this.getRootNode().host.closest("qti-test").querySelector("template[item-ref]");
733
- if (templateElement) this.myTemplate = prepareTemplate(templateElement);
734
760
  await this.updateComplete;
735
761
  this.dispatchEvent(
736
762
  new CustomEvent("qti-assessment-item-ref-connected", {
@@ -1667,7 +1693,7 @@ TestShowCorrectResponse = __decorateClass([
1667
1693
  // src/lib/qti-test/components/test-paging-buttons-stamp.ts
1668
1694
  import { html as html13, LitElement as LitElement13 } from "lit";
1669
1695
  import { customElement as customElement12 } from "lit/decorators.js";
1670
- import { prepareTemplate as prepareTemplate2 } from "@heximal/templates";
1696
+ import { prepareTemplate } from "@heximal/templates";
1671
1697
  import { consume as consume8 } from "@lit/context";
1672
1698
  var TestPagingButtonsStamp = class extends LitElement13 {
1673
1699
  createRenderRoot() {
@@ -1681,7 +1707,7 @@ var TestPagingButtonsStamp = class extends LitElement13 {
1681
1707
  connectedCallback() {
1682
1708
  super.connectedCallback();
1683
1709
  const templateElement = this.querySelector("template");
1684
- this.myTemplate = prepareTemplate2(templateElement);
1710
+ this.myTemplate = prepareTemplate(templateElement);
1685
1711
  }
1686
1712
  render() {
1687
1713
  if (!this.computedContext) return html13``;
@@ -1716,7 +1742,7 @@ var TestContainer = class extends LitElement14 {
1716
1742
  try {
1717
1743
  let api = await qtiTransformTest().load(this.testURL);
1718
1744
  const qtiTest = this.closest("qti-test");
1719
- if (qtiTest.postLoadTestTransformCallback) {
1745
+ if (qtiTest?.postLoadTestTransformCallback) {
1720
1746
  const tempDoc = api.htmlDoc();
1721
1747
  const testElement = tempDoc.querySelector("qti-assessment-test");
1722
1748
  if (testElement) {
@@ -1860,7 +1886,7 @@ TestPrintVariables = __decorateClass([
1860
1886
  // src/lib/qti-test/components/test-section-buttons-stamp.ts
1861
1887
  import { html as html16, LitElement as LitElement16 } from "lit";
1862
1888
  import { customElement as customElement15 } from "lit/decorators.js";
1863
- import { prepareTemplate as prepareTemplate3 } from "@heximal/templates";
1889
+ import { prepareTemplate as prepareTemplate2 } from "@heximal/templates";
1864
1890
  import { consume as consume10 } from "@lit/context";
1865
1891
  var TestSectionButtonsStamp = class extends LitElement16 {
1866
1892
  createRenderRoot() {
@@ -1874,7 +1900,7 @@ var TestSectionButtonsStamp = class extends LitElement16 {
1874
1900
  connectedCallback() {
1875
1901
  super.connectedCallback();
1876
1902
  const templateElement = this.querySelector("template");
1877
- this.myTemplate = prepareTemplate3(templateElement);
1903
+ this.myTemplate = prepareTemplate2(templateElement);
1878
1904
  }
1879
1905
  render() {
1880
1906
  if (!this.computedContext) return html16``;
@@ -1949,7 +1975,7 @@ TestPrintContext = __decorateClass([
1949
1975
  // src/lib/qti-test/components/test-stamp.ts
1950
1976
  import { html as html19, LitElement as LitElement19, nothing } from "lit";
1951
1977
  import { customElement as customElement18, property as property15, state as state5 } from "lit/decorators.js";
1952
- import { prepareTemplate as prepareTemplate4 } from "@heximal/templates";
1978
+ import { prepareTemplate as prepareTemplate3 } from "@heximal/templates";
1953
1979
  import { consume as consume12 } from "@lit/context";
1954
1980
  var TestStamp = class extends LitElement19 {
1955
1981
  constructor() {
@@ -1978,7 +2004,7 @@ var TestStamp = class extends LitElement19 {
1978
2004
  this.myTemplate = null;
1979
2005
  return;
1980
2006
  }
1981
- this.myTemplate = prepareTemplate4(templateElement);
2007
+ this.myTemplate = prepareTemplate3(templateElement);
1982
2008
  }
1983
2009
  willUpdate(_changedProperties) {
1984
2010
  if (!this.computedContext) {
@@ -2038,7 +2064,7 @@ TestStamp = __decorateClass([
2038
2064
  import { html as html20, LitElement as LitElement20, nothing as nothing2 } from "lit";
2039
2065
  import { consume as consume13 } from "@lit/context";
2040
2066
  import { customElement as customElement19, property as property16 } from "lit/decorators.js";
2041
- import { prepareTemplate as prepareTemplate5 } from "@heximal/templates";
2067
+ import { prepareTemplate as prepareTemplate4 } from "@heximal/templates";
2042
2068
  var TestScoringButtons = class extends LitElement20 {
2043
2069
  constructor() {
2044
2070
  super();
@@ -2063,7 +2089,7 @@ var TestScoringButtons = class extends LitElement20 {
2063
2089
  this.myTemplate = null;
2064
2090
  return;
2065
2091
  }
2066
- this.myTemplate = prepareTemplate5(templateElement);
2092
+ this.myTemplate = prepareTemplate4(templateElement);
2067
2093
  }
2068
2094
  _changeOutcomeScore(value) {
2069
2095
  const testPart = this.computedContext?.testParts.find((testPart2) => testPart2.active);
@@ -2109,7 +2135,7 @@ TestScoringButtons = __decorateClass([
2109
2135
  import { consume as consume14 } from "@lit/context";
2110
2136
  import { html as html21, LitElement as LitElement21, nothing as nothing3 } from "lit";
2111
2137
  import { customElement as customElement20 } from "lit/decorators.js";
2112
- import { prepareTemplate as prepareTemplate6 } from "@heximal/templates";
2138
+ import { prepareTemplate as prepareTemplate5 } from "@heximal/templates";
2113
2139
  var TestViewToggle = class extends LitElement21 {
2114
2140
  createRenderRoot() {
2115
2141
  return this;
@@ -2121,7 +2147,7 @@ var TestViewToggle = class extends LitElement21 {
2121
2147
  this.myTemplate = null;
2122
2148
  return;
2123
2149
  }
2124
- this.myTemplate = prepareTemplate6(templateElement);
2150
+ this.myTemplate = prepareTemplate5(templateElement);
2125
2151
  }
2126
2152
  _switchView(view) {
2127
2153
  this.dispatchEvent(
@@ -2263,4 +2289,4 @@ export {
2263
2289
  TestScoringFeedback,
2264
2290
  TestCheckItem
2265
2291
  };
2266
- //# sourceMappingURL=chunk-XI7S3HP2.js.map
2292
+ //# sourceMappingURL=chunk-XD7DR5YG.js.map