From ebadd6784b4e614a9bfe44ce4b3b993c6674f5f3 Mon Sep 17 00:00:00 2001 From: mjhayren Date: Tue, 3 Feb 2026 13:23:51 -0600 Subject: [PATCH 01/15] fixed the Graphic Gap Match - Solar System question --- bun.lock | 8 ++ .../GraphicGapMatchInteraction.svelte | 93 +++++++++++-------- .../routes/item-demo/[sample]/+page.svelte | 4 +- .../src/components/ItemBody.svelte | 8 +- 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/bun.lock b/bun.lock index 7dbedfd..ccab375 100644 --- a/bun.lock +++ b/bun.lock @@ -1950,10 +1950,14 @@ "@pie-qti/example/@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + "@pie-qti/i18n/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@pie-qti/item-player/node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], "@pie-qti/player-elements/@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + "@pie-qti/qti-common/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@pie-qti/test-utils/node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], "@pie-qti/transform-cli/node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], @@ -2048,8 +2052,12 @@ "@pie-qti/example/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + "@pie-qti/i18n/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@pie-qti/player-elements/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + "@pie-qti/qti-common/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@pie-qti/transform-web/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], diff --git a/packages/default-components/src/plugins/graphic-gap-match/GraphicGapMatchInteraction.svelte b/packages/default-components/src/plugins/graphic-gap-match/GraphicGapMatchInteraction.svelte index 29901c4..1358f19 100644 --- a/packages/default-components/src/plugins/graphic-gap-match/GraphicGapMatchInteraction.svelte +++ b/packages/default-components/src/plugins/graphic-gap-match/GraphicGapMatchInteraction.svelte @@ -40,9 +40,31 @@ let hoveredHotspotId = $state(null); let keyboardSelectedTextId = $state(null); // Gap text selected via keyboard let announceText = $state(''); // For screen reader announcements +// Get reference to the root element for event dispatching (needed for Shadow DOM) +let rootElement: HTMLDivElement | undefined = $state(); + +// Track if we're updating from internal change (user drag) vs external (prop update) +let isInternalUpdate = false; + +$effect(() => { + // Sync with parent response changes (only if not an internal update) + if (!isInternalUpdate) { + const newPairs = Array.isArray(parsedResponse) ? [...parsedResponse] : []; + console.log('[GraphicGapMatch] Syncing pairs from response:', { parsedResponse, newPairs, parsedInteraction: parsedInteraction?.responseId }); + pairs = newPairs; + } + isInternalUpdate = false; // Reset flag +}); + +// Debug: Log when component mounts/updates $effect(() => { - // Sync with parent response changes - pairs = Array.isArray(parsedResponse) ? [...parsedResponse] : []; + console.log('[GraphicGapMatch] Component state:', { + hasInteraction: !!parsedInteraction, + responseId: parsedInteraction?.responseId, + response: parsedResponse, + pairs, + disabled + }); }); // Get the hotspot matched to a gap text @@ -80,7 +102,11 @@ function isCorrectHotspot(hotspotId: string): boolean { } function handleDragStart(gapTextId: string) { - if (disabled) return; + if (disabled) { + console.log('[GraphicGapMatch] Drag start blocked - disabled'); + return; + } + console.log('[GraphicGapMatch] Drag start:', gapTextId); draggedTextId = gapTextId; } @@ -100,7 +126,7 @@ function handleHotspotDragLeave() { } function handleHotspotDrop(event: DragEvent, hotspotId: string) { - if (disabled || !draggedTextId) return; + if (disabled || !draggedTextId || !parsedInteraction) return; event.preventDefault(); // Remove any existing pair for this gapText @@ -112,21 +138,18 @@ function handleHotspotDrop(event: DragEvent, hotspotId: string) { // Add new pair newPairs.push(`${draggedTextId} ${hotspotId}`); + isInternalUpdate = true; // Mark as internal update to prevent sync effect from overwriting pairs = newPairs; response = pairs; // Call onChange callback if provided (for Svelte component usage) onChange?.(pairs); - // Dispatch custom event for web component usage - const event2 = new CustomEvent('qti-change', { - detail: { - responseId: parsedInteraction?.responseId, - value: pairs, - timestamp: Date.now(), - }, - bubbles: true, - composed: true, - }); - dispatchEvent(event2); + // Dispatch custom event for web component usage - dispatch from rootElement to ensure it bubbles out of Shadow DOM + const valueArray = Array.isArray(pairs) ? [...pairs] : []; + if (rootElement) { + const event2 = createQtiChangeEvent(parsedInteraction.responseId, valueArray); + console.log('[GraphicGapMatch] Dispatching qti-change:', { responseId: parsedInteraction.responseId, value: valueArray, pairsLength: valueArray.length }); + rootElement.dispatchEvent(event2); + } draggedTextId = null; hoveredHotspotId = null; @@ -138,21 +161,18 @@ function clearMatch(gapTextId: string) { const gapTextName = gapTextObj?.text || 'Label'; const newPairs = pairs.filter((p) => !p.startsWith(`${gapTextId} `)); + isInternalUpdate = true; // Mark as internal update pairs = newPairs; response = pairs; // Call onChange callback if provided (for Svelte component usage) onChange?.(pairs); - // Dispatch custom event for web component usage - const event = new CustomEvent('qti-change', { - detail: { - responseId: parsedInteraction?.responseId, - value: pairs, - timestamp: Date.now(), - }, - bubbles: true, - composed: true, - }); - dispatchEvent(event); + // Dispatch custom event for web component usage - dispatch from rootElement to ensure it bubbles out of Shadow DOM + const valueArray = Array.isArray(pairs) ? [...pairs] : []; + if (rootElement) { + const event = createQtiChangeEvent(parsedInteraction.responseId, valueArray); + console.log('[GraphicGapMatch] Dispatching qti-change (clearMatch):', { responseId: parsedInteraction.responseId, value: valueArray }); + rootElement.dispatchEvent(event); + } announceText = `${gapTextName} removed from hotspot`; } @@ -213,21 +233,18 @@ function placeSelectedLabelOnHotspot(hotspotId: string) { newPairs = newPairs.filter((p) => !p.endsWith(` ${hotspotId}`)); newPairs.push(`${keyboardSelectedTextId} ${hotspotId}`); + isInternalUpdate = true; // Mark as internal update pairs = newPairs; response = pairs; // Call onChange callback if provided (for Svelte component usage) onChange?.(pairs); - // Dispatch custom event for web component usage - const event2 = new CustomEvent('qti-change', { - detail: { - responseId: parsedInteraction?.responseId, - value: pairs, - timestamp: Date.now(), - }, - bubbles: true, - composed: true, - }); - dispatchEvent(event2); + // Dispatch custom event for web component usage - dispatch from rootElement to ensure it bubbles out of Shadow DOM + const valueArray = Array.isArray(pairs) ? [...pairs] : []; + if (rootElement) { + const event2 = createQtiChangeEvent(parsedInteraction.responseId, valueArray); + console.log('[GraphicGapMatch] Dispatching qti-change (keyboard):', { responseId: parsedInteraction.responseId, value: valueArray, length: valueArray.length }); + rootElement.dispatchEvent(event2); + } announceText = `${gapTextName} placed on hotspot ${hotspotIndex + 1}`; keyboardSelectedTextId = null; @@ -260,7 +277,7 @@ function parseCoords(hotspot: { identifier: string; shape: string; coords: strin -
+
{#if !parsedInteraction}
{i18n?.t('common.errorNoData', 'No interaction data provided')}
{:else} diff --git a/packages/example/src/routes/item-demo/[sample]/+page.svelte b/packages/example/src/routes/item-demo/[sample]/+page.svelte index 410b502..db82d07 100644 --- a/packages/example/src/routes/item-demo/[sample]/+page.svelte +++ b/packages/example/src/routes/item-demo/[sample]/+page.svelte @@ -113,10 +113,12 @@ } function handleResponseChange(responseId: string, value: any) { - console.log('[Demo] Response changed:', { responseId, value }); + console.log('[Demo] Response changed:', { responseId, value, valueType: typeof value, isArray: Array.isArray(value), arrayLength: Array.isArray(value) ? value.length : 'N/A' }); responses = { ...responses, [responseId]: value }; console.log('[Demo] All responses:', responses); if (player) { + const validation = player.validateResponses(responses); + console.log('[Demo] Validation result:', validation); const canSubmit = player.canSubmitResponses(responses); console.log('[Demo] Can submit:', canSubmit); } diff --git a/packages/item-player/src/components/ItemBody.svelte b/packages/item-player/src/components/ItemBody.svelte index b0e58eb..b16abd7 100644 --- a/packages/item-player/src/components/ItemBody.svelte +++ b/packages/item-player/src/components/ItemBody.svelte @@ -181,6 +181,7 @@ // Handle qti:change events from web components function handleQtiChange(event: CustomEvent) { + console.log('[ItemBody] Received qti-change event:', event.detail); const { responseId, value } = event.detail; handleResponseChange(responseId, value); } @@ -190,10 +191,15 @@ let rootEl: HTMLDivElement | null = $state(null); $effect(() => { if (!rootEl) return; - const handler = (e: Event) => handleQtiChange(e as CustomEvent); + const handler = (e: Event) => { + console.log('[ItemBody] Event listener triggered:', e.type, e.target, (e as CustomEvent).detail); + handleQtiChange(e as CustomEvent); + }; const el = rootEl; // Capture reference for cleanup + console.log('[ItemBody] Adding qti-change listener to rootEl'); el.addEventListener('qti-change', handler as EventListener); return () => { + console.log('[ItemBody] Removing qti-change listener from rootEl'); el.removeEventListener('qti-change', handler as EventListener); }; }); From 7ad0fb8dc03c6482c70d7eed90e0bb8aea6fcb47 Mon Sep 17 00:00:00 2001 From: mjhayren Date: Tue, 3 Feb 2026 13:52:46 -0600 Subject: [PATCH 02/15] shaped up items selected value and results rollup Found an issue in the hottext inteaction - multiple selections item type. Changed the value of 0/3 to 0/4 being theres 4 options in total to select Also fixed the results rollup for all items so that when you test it out, it increases number of attempts, whether it was completed or not --- .../src/plugins/hottext/HottextInteraction.svelte | 2 +- packages/item-player/src/core/Player.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/default-components/src/plugins/hottext/HottextInteraction.svelte b/packages/default-components/src/plugins/hottext/HottextInteraction.svelte index cbe1c22..499e377 100644 --- a/packages/default-components/src/plugins/hottext/HottextInteraction.svelte +++ b/packages/default-components/src/plugins/hottext/HottextInteraction.svelte @@ -218,7 +218,7 @@ {/if} - {#if parsedInteraction.maxPlays > 0} + {#if parsedInteraction.minPlays > 0 && !hasMetMinPlays}
Remaining: - {Math.max(0, parsedInteraction.maxPlays - playCount)} + {Math.max(0, parsedInteraction.minPlays - playCount)}
{/if}
From bd3cc089ffb3866bfb679dbc8c7ba064ac5bd4c0 Mon Sep 17 00:00:00 2001 From: mjhayren Date: Fri, 13 Feb 2026 09:59:07 -0600 Subject: [PATCH 06/15] all test items working, commit prior to refactor --- bun.lock | 12 ++-- .../gap-match/GapMatchInteraction.svelte | 35 +++++++--- .../CustomInteractionFallback.svelte | 2 +- .../shared/components/MatchDragDrop.svelte | 47 +++++++------- .../src/shared/utils/pairHelpers.ts | 41 +++++++++--- .../src/lib/sample-items-edge-cases.ts | 14 ++-- packages/example/src/lib/sample-items.ts | 12 +++- .../routes/item-demo/[sample]/+page.svelte | 65 +++++++++++++++++-- .../[sample]/components/QuestionPanel.svelte | 9 ++- packages/i18n/src/locales/en-US.ts | 2 +- packages/item-player/src/core/Player.ts | 10 +++ 11 files changed, 180 insertions(+), 69 deletions(-) diff --git a/bun.lock b/bun.lock index f12c6d7..02fc03f 100644 --- a/bun.lock +++ b/bun.lock @@ -1889,7 +1889,7 @@ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - "@acme/likert-scale-plugin/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@acme/likert-scale-plugin/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -1953,13 +1953,13 @@ "@pie-qti/example/@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], - "@pie-qti/i18n/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@pie-qti/i18n/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@pie-qti/item-player/node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], "@pie-qti/player-elements/@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], - "@pie-qti/qti-common/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@pie-qti/qti-common/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@pie-qti/test-utils/node-html-parser": ["node-html-parser@7.0.2", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ=="], @@ -2003,7 +2003,7 @@ "unzipper/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - "@acme/likert-scale-plugin/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@acme/likert-scale-plugin/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -2057,11 +2057,11 @@ "@pie-qti/example/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], - "@pie-qti/i18n/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@pie-qti/i18n/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "@pie-qti/player-elements/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], - "@pie-qti/qti-common/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@pie-qti/qti-common/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "@pie-qti/transform-web/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], diff --git a/packages/default-components/src/plugins/gap-match/GapMatchInteraction.svelte b/packages/default-components/src/plugins/gap-match/GapMatchInteraction.svelte index 76c9fa6..dcfa28f 100644 --- a/packages/default-components/src/plugins/gap-match/GapMatchInteraction.svelte +++ b/packages/default-components/src/plugins/gap-match/GapMatchInteraction.svelte @@ -37,29 +37,44 @@ // Track cleanup functions for event listeners to prevent memory leaks let cleanupFunctions: (() => void)[] = []; - function handleGapChange(gapId: string, wordId: string) { - // Remove any existing pair for this gap - const newPairs = pairs.filter((p: string) => !p.endsWith(` ${gapId}`)); + function getMatchMax(wordId: string): number { + const gt = parsedInteraction?.gapTexts?.find((g) => g.identifier === wordId); + return gt?.matchMax ?? 1; + } - // If this word is already used in another gap, move it (remove its previous assignment). - const withoutWord = newPairs.filter((p: string) => !p.startsWith(`${wordId} `)); + function handleGapChange(gapId: string, wordId: string) { + // Remove any existing pair for this gap (each gap holds one word) + let newPairs = pairs.filter((p: string) => !p.endsWith(` ${gapId}`)); + + // For reusable words (matchMax=0 or >1): don't remove other placements. For matchMax=1: move the word. + const matchMax = getMatchMax(wordId); + const currentCount = newPairs.filter((p: string) => p.startsWith(`${wordId} `)).length; + if (matchMax === 1 || (matchMax > 0 && currentCount >= matchMax)) { + // Single-use or at limit: remove this word from other gaps (move) + newPairs = newPairs.filter((p: string) => !p.startsWith(`${wordId} `)); + } // Add new pair if a word was selected if (wordId) { - withoutWord.push(`${wordId} ${gapId}`); + const newPair = `${wordId} ${gapId}`; + if (!newPairs.includes(newPair)) { + newPairs.push(newPair); + } } - response = withoutWord; + response = newPairs; // Call onChange callback if provided (for Svelte component usage) - onChange?.(withoutWord); + onChange?.(newPairs); // Dispatch event for web component usage - event will bubble up to the host element if (rootElement) { - rootElement.dispatchEvent(createQtiChangeEvent(parsedInteraction?.responseId, withoutWord)); + rootElement.dispatchEvent(createQtiChangeEvent(parsedInteraction?.responseId, newPairs)); } } function isWordUsed(wordId: string): boolean { - return pairs.some((p: string) => p.startsWith(wordId)); + const matchMax = parsedInteraction?.gapTexts?.find((g) => g.identifier === wordId)?.matchMax ?? 1; + const count = pairs.filter((p: string) => p.startsWith(`${wordId} `)).length; + return matchMax > 0 ? count >= matchMax : false; // matchMax=0 means unlimited, never "used" } function getSelectedWord(gapId: string): string { diff --git a/packages/default-components/src/shared/components/CustomInteractionFallback.svelte b/packages/default-components/src/shared/components/CustomInteractionFallback.svelte index 147623c..114625f 100644 --- a/packages/default-components/src/shared/components/CustomInteractionFallback.svelte +++ b/packages/default-components/src/shared/components/CustomInteractionFallback.svelte @@ -47,7 +47,7 @@
-
{i18n?.t('interactions.custom.unsupported') ?? 'Unsupported customInteraction'}
+
{i18n?.t('interactions.custom.unsupported') ?? 'Custom Interaction (Currently Unsupported)'}
This item contains a vendor-specific interaction. This player does not execute custom interactions.
diff --git a/packages/default-components/src/shared/components/MatchDragDrop.svelte b/packages/default-components/src/shared/components/MatchDragDrop.svelte index 59bbc36..c80a073 100644 --- a/packages/default-components/src/shared/components/MatchDragDrop.svelte +++ b/packages/default-components/src/shared/components/MatchDragDrop.svelte @@ -7,7 +7,7 @@ import type { AssociableChoice } from '@pie-qti/item-player'; import type { I18nProvider } from '@pie-qti/i18n'; -import { createOrUpdatePair, getSourceForTarget, getTargetForSource, removePairBySource } from '../utils/pairHelpers.js'; +import { createOrUpdatePair, getSourceForTarget, getTargetsForSource, removePairBySource } from '../utils/pairHelpers.js'; import { touchDrag } from '../utils/touchDragHelper.js'; import DragHandle from './DragHandle.svelte'; import '../styles/shared.css'; @@ -136,63 +136,64 @@ function clearMatch(sourceId: string) { {i18n?.t('interactions.match.dragFromHere') ?? 'Drag from here:'} {#each sourceSet as source (source.identifier)} - {@const matchedTarget = getTargetForSource(pairs, source.identifier)} - {@const targetItem = matchedTarget ? targetSet.find((t) => t.identifier === matchedTarget) : null} + {@const matchedTargets = getTargetsForSource(pairs, source.identifier)} + {@const targetItems = matchedTargets.map((tid) => targetSet.find((t) => t.identifier === tid)).filter(Boolean)} {@const isSelected = keyboardSelectedSourceId === source.identifier} - {@const correctTarget = getTargetForSource(correctPairs, source.identifier)} - {@const isCorrect = correctTarget !== null} + {@const correctTargets = getTargetsForSource(correctPairs, source.identifier)} + {@const isCorrect = correctTargets.length > 0} + {@const canDragMore = matchedTargets.length < (source.matchMax ?? 1)}
- {#if matchedTarget && !disabled} + {#if matchedTargets.length > 0 && !disabled}
@@ -77,14 +74,14 @@
- {i18n?.t('transform.admin.plugins.vendorExtensions', 'Vendor Extensions')} + {(i18n?.t('transform.admin.plugins.vendorExtensions') ?? 'Vendor Extensions')}
{totalExtensions}
-
{i18n?.t('transform.admin.plugins.extensionsRegistered', 'Registered')}
+
{(i18n?.t('transform.admin.plugins.extensionsRegistered') ?? 'Registered')}
-
{i18n?.t('transform.admin.plugins.storageBackend', 'Storage Backend')}
+
{(i18n?.t('transform.admin.plugins.storageBackend') ?? 'Storage Backend')}
{data.storageInfo.backend}
{data.storageInfo.type}
@@ -94,24 +91,21 @@

- {i18n?.t('transform.admin.plugins.installedPlugins', 'Installed Transform Plugins')} + {(i18n?.t('transform.admin.plugins.installedPlugins') ?? 'Installed Transform Plugins')}

- {i18n?.t( - 'transform.admin.plugins.pluginsDescription', - 'These plugins handle transformations between different formats' - )} + {(i18n?.t('transform.admin.plugins.pluginsDescription') ?? 'These plugins handle transformations between different formats')}

- - - - - + + + + + @@ -136,7 +130,7 @@ @@ -151,13 +145,10 @@

- {i18n?.t('transform.admin.plugins.vendorExtensionPoints', 'Vendor Extension Points')} + {(i18n?.t('transform.admin.plugins.vendorExtensionPoints') ?? 'Vendor Extension Points')}

- {i18n?.t( - 'transform.admin.plugins.extensionsDescription', - 'Vendor-specific extensions registered in plugins' - )} + {(i18n?.t('transform.admin.plugins.extensionsDescription') ?? 'Vendor-specific extensions registered in plugins')}

@@ -180,13 +171,10 @@

- {i18n?.t('transform.admin.plugins.availableExtensionPoints', 'Available Extension Points')} + {(i18n?.t('transform.admin.plugins.availableExtensionPoints') ?? 'Available Extension Points')}

- {i18n?.t( - 'transform.admin.plugins.configureExtensions', - 'Configure these via config.json or environment variables' - )} + {(i18n?.t('transform.admin.plugins.configureExtensions') ?? 'Configure these via config.json or environment variables')}

@@ -194,7 +182,7 @@
- {i18n?.t('transform.admin.plugins.storageBackends', 'Storage Backends')} ({data.extensionPoints.storageBackends.length} available) + {(i18n?.t('transform.admin.plugins.storageBackends') ?? 'Storage Backends')} ({data.extensionPoints.storageBackends.length} available)
@@ -203,10 +191,7 @@ {/each}

- {i18n?.t( - 'transform.admin.plugins.storageDescription', - 'Choose where to store sessions and transformed content' - )} + {(i18n?.t('transform.admin.plugins.storageDescription') ?? 'Choose where to store sessions and transformed content')}

@@ -215,7 +200,7 @@
- {i18n?.t('transform.admin.plugins.transformFormats', 'Transform Formats')} ({data.extensionPoints.transformFormats.length} supported) + {(i18n?.t('transform.admin.plugins.transformFormats') ?? 'Transform Formats')} ({data.extensionPoints.transformFormats.length} supported)
@@ -224,10 +209,7 @@ {/each}

- {i18n?.t( - 'transform.admin.plugins.formatsDescription', - 'Supported input and output formats for transformations' - )} + {(i18n?.t('transform.admin.plugins.formatsDescription') ?? 'Supported input and output formats for transformations')}

@@ -236,7 +218,7 @@
- {i18n?.t('transform.admin.plugins.vendorExtensionTypes', 'Vendor Extension Types')} ({data.extensionPoints.vendorExtensionTypes.length} types) + {(i18n?.t('transform.admin.plugins.vendorExtensionTypes') ?? 'Vendor Extension Types')} ({data.extensionPoints.vendorExtensionTypes.length} types)
@@ -245,10 +227,7 @@ {/each}

- {i18n?.t( - 'transform.admin.plugins.vendorTypesDescription', - 'Customize transformation behavior for specific vendors' - )} + {(i18n?.t('transform.admin.plugins.vendorTypesDescription') ?? 'Customize transformation behavior for specific vendors')}

@@ -257,7 +236,7 @@
- {i18n?.t('transform.admin.plugins.uiThemes', 'UI Themes')} ({data.extensionPoints.themes.length} + {(i18n?.t('transform.admin.plugins.uiThemes') ?? 'UI Themes')} ({data.extensionPoints.themes.length} available)
@@ -267,10 +246,7 @@ {/each}

- {i18n?.t( - 'transform.admin.plugins.themesDescription', - 'Customize the application appearance' - )} + {(i18n?.t('transform.admin.plugins.themesDescription') ?? 'Customize the application appearance')}

@@ -279,7 +255,7 @@
- {i18n?.t('transform.admin.plugins.locales', 'Locales')} ({data.extensionPoints.locales.length} + {(i18n?.t('transform.admin.plugins.locales') ?? 'Locales')} ({data.extensionPoints.locales.length} supported)
@@ -289,10 +265,7 @@ {/each}

- {i18n?.t( - 'transform.admin.plugins.localesDescription', - 'Add translations for different languages' - )} + {(i18n?.t('transform.admin.plugins.localesDescription') ?? 'Add translations for different languages')}

@@ -316,28 +289,25 @@ >
-

{i18n?.t('transform.admin.plugins.howToConfigure', 'How to Configure')}

+

{(i18n?.t('transform.admin.plugins.howToConfigure') ?? 'How to Configure')}

- {i18n?.t( - 'transform.admin.plugins.configureVia', - 'Configure plugins and extensions through these methods:' - )} + {(i18n?.t('transform.admin.plugins.configureVia') ?? 'Configure plugins and extensions through these methods:')}

  • - {i18n?.t('transform.admin.plugins.envVar', 'Environment variable')}: + {(i18n?.t('transform.admin.plugins.envVar') ?? 'Environment variable')}: PIE_QTI_CONFIG=/path/to/config.json
  • - {i18n?.t('transform.admin.plugins.configFile', 'Config file')}: - {i18n?.t('transform.admin.plugins.seeExample', 'See')} + {(i18n?.t('transform.admin.plugins.configFile') ?? 'Config file')}: + {(i18n?.t('transform.admin.plugins.seeExample') ?? 'See')} config.example.json - {i18n?.t('transform.admin.plugins.forStructure', 'for structure')} + {(i18n?.t('transform.admin.plugins.forStructure') ?? 'for structure')}
  • - {i18n?.t('transform.admin.plugins.directCode', 'Direct code')}: - {i18n?.t('transform.admin.plugins.registerIn', 'Register in')} + {(i18n?.t('transform.admin.plugins.directCode') ?? 'Direct code')}: + {(i18n?.t('transform.admin.plugins.registerIn') ?? 'Register in')} src/hooks.server.ts
{i18n?.t('transform.admin.plugins.pluginName', 'Plugin Name')}{i18n?.t('transform.admin.plugins.format', 'Format')}{i18n?.t('transform.admin.plugins.priority', 'Priority')}{i18n?.t('transform.admin.plugins.version', 'Version')}{i18n?.t('transform.admin.plugins.status', 'Status')}{(i18n?.t('transform.admin.plugins.pluginName') ?? 'Plugin Name')}{(i18n?.t('transform.admin.plugins.format') ?? 'Format')}{(i18n?.t('transform.admin.plugins.priority') ?? 'Priority')}{(i18n?.t('transform.admin.plugins.version') ?? 'Version')}{(i18n?.t('transform.admin.plugins.status') ?? 'Status')}
{plugin.version} - {i18n?.t('transform.admin.plugins.active', 'Active')} + {(i18n?.t('transform.admin.plugins.active') ?? 'Active')}