@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.
- package/LICENSE.md +674 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/interface.d.ts +202 -0
- package/dist/interface.js +2 -0
- package/dist/interface.js.map +1 -0
- package/dist/qti-portable-custom-interaction.d.ts +115 -0
- package/dist/qti-portable-custom-interaction.js +1848 -0
- package/dist/qti-portable-custom-interaction.js.map +1 -0
- package/dist/qti-portable-custom-interaction.styles.d.ts +2 -0
- package/dist/qti-portable-custom-interaction.styles.js +15 -0
- package/dist/qti-portable-custom-interaction.styles.js.map +1 -0
- package/dist/qti-portable-custom-test-interaction.d.ts +84 -0
- package/dist/qti-portable-custom-test-interaction.js +492 -0
- package/dist/qti-portable-custom-test-interaction.js.map +1 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +5 -0
- package/dist/register.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';
|
|
2
|
+
/**
|
|
3
|
+
* Test-specific extension of QtiPortableCustomInteraction that adds methods
|
|
4
|
+
* for interacting with the iframe content in tests.
|
|
5
|
+
*/
|
|
6
|
+
export class QtiPortableCustomInteractionTest extends QtiPortableCustomInteraction {
|
|
7
|
+
#recreatingIframe = false;
|
|
8
|
+
/**
|
|
9
|
+
* Gets the HTML content of the iframe for testing purposes
|
|
10
|
+
* @returns Promise that resolves with the HTML content string
|
|
11
|
+
*/
|
|
12
|
+
async getIFrameContent() {
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
const messageId = `get-content-${Date.now()}`;
|
|
15
|
+
const messageHandler = (event) => {
|
|
16
|
+
const { data } = event;
|
|
17
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
18
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
19
|
+
data?.method === 'getContentResponse' &&
|
|
20
|
+
data?.messageId === messageId) {
|
|
21
|
+
window.removeEventListener('message', messageHandler);
|
|
22
|
+
resolve(data.content);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener('message', messageHandler);
|
|
26
|
+
this.sendMessageToIframe('getContent', { messageId });
|
|
27
|
+
// Set timeout to avoid hanging promises
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
window.removeEventListener('message', messageHandler);
|
|
30
|
+
resolve('');
|
|
31
|
+
}, 5000);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Expose the protected context
|
|
35
|
+
setTestContext(newContext) {
|
|
36
|
+
this.context = newContext;
|
|
37
|
+
// If needed, force an update to push the context changes to the iframe
|
|
38
|
+
this.updateComplete.then(() => {
|
|
39
|
+
// const responseVal = this.responseVariablesToQtiVariableJSON(
|
|
40
|
+
// (newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.value as string | string[]) ||
|
|
41
|
+
// null,
|
|
42
|
+
// newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.cardinality || 'single',
|
|
43
|
+
// newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.baseType || 'string'
|
|
44
|
+
// );
|
|
45
|
+
this.value = newContext?.variables?.find(v => v.identifier === this.responseIdentifier).value;
|
|
46
|
+
// this.recreateIframe();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Recreates the iframe completely
|
|
51
|
+
* @returns Promise that resolves when the iframe is loaded
|
|
52
|
+
*/
|
|
53
|
+
async recreateIframe() {
|
|
54
|
+
this.#recreatingIframe = true;
|
|
55
|
+
try {
|
|
56
|
+
// Remove existing iframe if it exists
|
|
57
|
+
if (this.iframe) {
|
|
58
|
+
this._iframeLoaded = false;
|
|
59
|
+
this.iframe.remove();
|
|
60
|
+
}
|
|
61
|
+
this._pendingMessages = [];
|
|
62
|
+
// Create a new iframe
|
|
63
|
+
this.createIframe();
|
|
64
|
+
// Wait for iframe to load
|
|
65
|
+
return new Promise(resolve => {
|
|
66
|
+
const loadHandler = () => {
|
|
67
|
+
this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
this.addEventListener('qti-portable-custom-interaction-loaded', loadHandler);
|
|
71
|
+
// Add timeout to avoid hanging if the event never fires
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);
|
|
74
|
+
resolve();
|
|
75
|
+
}, 5000);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.#recreatingIframe = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Override the original disconnectedCallback to handle iframe recreation
|
|
84
|
+
*/
|
|
85
|
+
disconnectedCallback() {
|
|
86
|
+
// Only call super if we're not in the process of recreating the iframe
|
|
87
|
+
// This prevents removing event listeners that we still need
|
|
88
|
+
if (!this.#recreatingIframe) {
|
|
89
|
+
super.disconnectedCallback();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Simulates a mouse click at specific coordinates within the iframe
|
|
94
|
+
* @param x The x coordinate
|
|
95
|
+
* @param y The y coordinate
|
|
96
|
+
* @returns Promise that resolves when the click is performed
|
|
97
|
+
*/
|
|
98
|
+
async iFrameMouseClick(x, y) {
|
|
99
|
+
return new Promise(resolve => {
|
|
100
|
+
const messageId = `click-${Date.now()}`;
|
|
101
|
+
const messageHandler = (event) => {
|
|
102
|
+
const { data } = event;
|
|
103
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
104
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
105
|
+
data?.method === 'clickResponse' &&
|
|
106
|
+
data?.messageId === messageId) {
|
|
107
|
+
window.removeEventListener('message', messageHandler);
|
|
108
|
+
resolve();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener('message', messageHandler);
|
|
112
|
+
this.sendMessageToIframe('simulateClick', { x, y, messageId });
|
|
113
|
+
// Set timeout to avoid hanging promises
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
window.removeEventListener('message', messageHandler);
|
|
116
|
+
resolve();
|
|
117
|
+
}, 5000);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clicks on an element identified by a CSS selector
|
|
122
|
+
* @param selector The CSS selector string
|
|
123
|
+
* @returns Promise that resolves with boolean indicating if the element was found and clicked
|
|
124
|
+
*/
|
|
125
|
+
async iFrameClickOnElement(selector) {
|
|
126
|
+
return new Promise(resolve => {
|
|
127
|
+
const messageId = `click-selector-${Date.now()}`;
|
|
128
|
+
const messageHandler = (event) => {
|
|
129
|
+
const { data } = event;
|
|
130
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
131
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
132
|
+
data?.method === 'clickSelectorResponse' &&
|
|
133
|
+
data?.messageId === messageId) {
|
|
134
|
+
window.removeEventListener('message', messageHandler);
|
|
135
|
+
resolve(data.success);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
window.addEventListener('message', messageHandler);
|
|
139
|
+
this.sendMessageToIframe('clickOnSelector', { selector, messageId });
|
|
140
|
+
// Set timeout to avoid hanging promises
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
window.removeEventListener('message', messageHandler);
|
|
143
|
+
resolve(false);
|
|
144
|
+
}, 5000);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Clicks on an element containing the specified text
|
|
149
|
+
* @param text The text to search for within elements
|
|
150
|
+
* @returns Promise that resolves with boolean indicating if the element was found and clicked
|
|
151
|
+
*/
|
|
152
|
+
async iFrameClickOnElementByText(text) {
|
|
153
|
+
return new Promise(resolve => {
|
|
154
|
+
const messageId = `click-text-${Date.now()}`;
|
|
155
|
+
const messageHandler = (event) => {
|
|
156
|
+
const { data } = event;
|
|
157
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
158
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
159
|
+
data?.method === 'clickTextResponse' &&
|
|
160
|
+
data?.messageId === messageId) {
|
|
161
|
+
window.removeEventListener('message', messageHandler);
|
|
162
|
+
resolve(data.success);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
window.addEventListener('message', messageHandler);
|
|
166
|
+
this.sendMessageToIframe('clickOnElementByText', { text, messageId });
|
|
167
|
+
// Set timeout to avoid hanging promises
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
window.removeEventListener('message', messageHandler);
|
|
170
|
+
resolve(false);
|
|
171
|
+
}, 5000);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sets the value of an input element identified by a CSS selector
|
|
176
|
+
* @param selector The CSS selector string
|
|
177
|
+
* @param value The value to set
|
|
178
|
+
* @returns Promise that resolves with boolean indicating if the element was found and value was set
|
|
179
|
+
*/
|
|
180
|
+
async iFrameSetValueElement(selector, value) {
|
|
181
|
+
return new Promise(resolve => {
|
|
182
|
+
const messageId = `set-value-${Date.now()}`;
|
|
183
|
+
const messageHandler = (event) => {
|
|
184
|
+
const { data } = event;
|
|
185
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
186
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
187
|
+
data?.method === 'setValueResponse' &&
|
|
188
|
+
data?.messageId === messageId) {
|
|
189
|
+
window.removeEventListener('message', messageHandler);
|
|
190
|
+
resolve(data.success);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
window.addEventListener('message', messageHandler);
|
|
194
|
+
this.sendMessageToIframe('setValueElement', { selector, value, messageId });
|
|
195
|
+
// Set timeout to avoid hanging promises
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
window.removeEventListener('message', messageHandler);
|
|
198
|
+
resolve(false);
|
|
199
|
+
}, 5000);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Gets the bounding client rect for an element inside the iframe
|
|
204
|
+
* @param selector The CSS selector string
|
|
205
|
+
* @returns Promise that resolves with the rect or null
|
|
206
|
+
*/
|
|
207
|
+
async iFrameGetBoundingClientRect(selector) {
|
|
208
|
+
return new Promise(resolve => {
|
|
209
|
+
const messageId = `get-bounding-rect-${Date.now()}`;
|
|
210
|
+
const messageHandler = (event) => {
|
|
211
|
+
const { data } = event;
|
|
212
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
213
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
214
|
+
data?.method === 'getBoundingRectResponse' &&
|
|
215
|
+
data?.messageId === messageId) {
|
|
216
|
+
window.removeEventListener('message', messageHandler);
|
|
217
|
+
resolve(data.rect ?? null);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
window.addEventListener('message', messageHandler);
|
|
221
|
+
this.sendMessageToIframe('getBoundingRect', { selector, messageId });
|
|
222
|
+
setTimeout(() => {
|
|
223
|
+
window.removeEventListener('message', messageHandler);
|
|
224
|
+
resolve(null);
|
|
225
|
+
}, 5000);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Sets the value of an input element containing the specified text
|
|
230
|
+
* @param text The text to search for within elements
|
|
231
|
+
* @param value The value to set
|
|
232
|
+
* @returns Promise that resolves with boolean indicating if the element was found and value was set
|
|
233
|
+
*/
|
|
234
|
+
async iFrameSetValueElementByText(text, value) {
|
|
235
|
+
return new Promise(resolve => {
|
|
236
|
+
const messageId = `set-value-text-${Date.now()}`;
|
|
237
|
+
const messageHandler = (event) => {
|
|
238
|
+
const { data } = event;
|
|
239
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
240
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
241
|
+
data?.method === 'setValueByTextResponse' &&
|
|
242
|
+
data?.messageId === messageId) {
|
|
243
|
+
window.removeEventListener('message', messageHandler);
|
|
244
|
+
resolve(data.success);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
window.addEventListener('message', messageHandler);
|
|
248
|
+
this.sendMessageToIframe('setValueElementByText', { text, value, messageId });
|
|
249
|
+
// Set timeout to avoid hanging promises
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
window.removeEventListener('message', messageHandler);
|
|
252
|
+
resolve(false);
|
|
253
|
+
}, 5000);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Performs a mousedown event on an element identified by a CSS selector
|
|
258
|
+
* @param selector The CSS selector string
|
|
259
|
+
* @returns Promise that resolves with boolean indicating if the element was found and mousedown was performed
|
|
260
|
+
*/
|
|
261
|
+
async iFrameMousedownOnElement(selector) {
|
|
262
|
+
return new Promise(resolve => {
|
|
263
|
+
const messageId = `mousedown-selector-${Date.now()}`;
|
|
264
|
+
const messageHandler = (event) => {
|
|
265
|
+
const { data } = event;
|
|
266
|
+
if (data?.source === 'qti-pci-iframe' &&
|
|
267
|
+
(!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&
|
|
268
|
+
data?.method === 'mousedownSelectorResponse' &&
|
|
269
|
+
data?.messageId === messageId) {
|
|
270
|
+
window.removeEventListener('message', messageHandler);
|
|
271
|
+
resolve(data.success);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
window.addEventListener('message', messageHandler);
|
|
275
|
+
this.sendMessageToIframe('mousedownOnSelector', { selector, messageId });
|
|
276
|
+
// Set timeout to avoid hanging promises
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
window.removeEventListener('message', messageHandler);
|
|
279
|
+
resolve(false);
|
|
280
|
+
}, 5000);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Override the original generateIframeContent method to include
|
|
285
|
+
* the test message handlers in the iframe HTML
|
|
286
|
+
*/
|
|
287
|
+
generateIframeContent() {
|
|
288
|
+
// Get the original iframe HTML from the parent class
|
|
289
|
+
const originalContent = super.generateIframeContent();
|
|
290
|
+
// Insert our test message handlers before the closing </script> tag
|
|
291
|
+
const testHandlers = `
|
|
292
|
+
// Test helper functions for QtiPortableCustomInteractionTest
|
|
293
|
+
// Base iframe implementation already provides PCIManager.getContent
|
|
294
|
+
|
|
295
|
+
// Base iframe implementation already provides PCIManager.simulateClick
|
|
296
|
+
|
|
297
|
+
// Base iframe implementation already provides PCIManager.clickOnSelector
|
|
298
|
+
|
|
299
|
+
// Base iframe implementation already provides PCIManager.clickOnElementByText
|
|
300
|
+
|
|
301
|
+
// Base iframe implementation already provides PCIManager.setValueElement
|
|
302
|
+
|
|
303
|
+
function deepQuerySelector(root, selector) {
|
|
304
|
+
if (!root) return null;
|
|
305
|
+
try {
|
|
306
|
+
const direct = root.querySelector ? root.querySelector(selector) : null;
|
|
307
|
+
if (direct) return direct;
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// ignore invalid selector for this root
|
|
310
|
+
}
|
|
311
|
+
if (!root.querySelectorAll) return null;
|
|
312
|
+
const nodes = root.querySelectorAll('*');
|
|
313
|
+
for (const node of nodes) {
|
|
314
|
+
if (node && node.shadowRoot) {
|
|
315
|
+
const found = deepQuerySelector(node.shadowRoot, selector);
|
|
316
|
+
if (found) return found;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function deepFindElementByExactText(root, text) {
|
|
323
|
+
if (!root || !text) return null;
|
|
324
|
+
if (root.querySelectorAll) {
|
|
325
|
+
const nodes = root.querySelectorAll('*');
|
|
326
|
+
for (const node of nodes) {
|
|
327
|
+
if ((node.textContent || '').trim() === text) return node;
|
|
328
|
+
if (node.shadowRoot) {
|
|
329
|
+
const found = deepFindElementByExactText(node.shadowRoot, text);
|
|
330
|
+
if (found) return found;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function escapeSelectorId(id) {
|
|
338
|
+
try {
|
|
339
|
+
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(id);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
// ignore
|
|
342
|
+
}
|
|
343
|
+
return String(id).replace(/([ #;?%&,.+*~\\':"!^$[\\]()=>|\\/])/g, '\\\\$1');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
PCIManager.setValueElementByText = function(params) {
|
|
347
|
+
const messageId = params && params.messageId;
|
|
348
|
+
try {
|
|
349
|
+
const text = params && params.text;
|
|
350
|
+
const value = params && params.value;
|
|
351
|
+
|
|
352
|
+
const textEl = text ? deepFindElementByExactText(document, text) : null;
|
|
353
|
+
let target = null;
|
|
354
|
+
|
|
355
|
+
if (textEl) {
|
|
356
|
+
const tag = (textEl.tagName || '').toUpperCase();
|
|
357
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
|
358
|
+
target = textEl;
|
|
359
|
+
} else if (tag === 'LABEL' && textEl.htmlFor) {
|
|
360
|
+
const id = escapeSelectorId(textEl.htmlFor);
|
|
361
|
+
target = deepQuerySelector(document, '#' + id);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!target) {
|
|
365
|
+
let parent = textEl.parentElement;
|
|
366
|
+
while (parent) {
|
|
367
|
+
const candidate = parent.querySelector ? parent.querySelector('input, textarea, select') : null;
|
|
368
|
+
if (candidate) {
|
|
369
|
+
target = candidate;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
parent = parent.parentElement;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const success = !!target && setValueOnElement(target, value);
|
|
378
|
+
window.parent.postMessage(
|
|
379
|
+
{
|
|
380
|
+
source: 'qti-pci-iframe',
|
|
381
|
+
responseIdentifier: PCIManager.responseIdentifier,
|
|
382
|
+
method: 'setValueByTextResponse',
|
|
383
|
+
messageId: messageId,
|
|
384
|
+
success: success
|
|
385
|
+
},
|
|
386
|
+
'*'
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('Error setting value by text:', error);
|
|
390
|
+
window.parent.postMessage(
|
|
391
|
+
{
|
|
392
|
+
source: 'qti-pci-iframe',
|
|
393
|
+
responseIdentifier: PCIManager.responseIdentifier,
|
|
394
|
+
method: 'setValueByTextResponse',
|
|
395
|
+
messageId: messageId,
|
|
396
|
+
success: false,
|
|
397
|
+
error: error.toString()
|
|
398
|
+
},
|
|
399
|
+
'*'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
PCIManager.mousedownOnSelector = function(params) {
|
|
405
|
+
try {
|
|
406
|
+
const element = params && params.selector ? deepQuerySelector(document, params.selector) : null;
|
|
407
|
+
|
|
408
|
+
let success = false;
|
|
409
|
+
|
|
410
|
+
if (element) {
|
|
411
|
+
const rect = element.getBoundingClientRect();
|
|
412
|
+
const mousedownEvent = new MouseEvent('mousedown', {
|
|
413
|
+
bubbles: true,
|
|
414
|
+
cancelable: true,
|
|
415
|
+
view: window,
|
|
416
|
+
clientX: rect.left + rect.width / 2,
|
|
417
|
+
clientY: rect.top + rect.height / 2
|
|
418
|
+
});
|
|
419
|
+
element.dispatchEvent(mousedownEvent);
|
|
420
|
+
success = true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
window.parent.postMessage({
|
|
424
|
+
source: 'qti-pci-iframe',
|
|
425
|
+
responseIdentifier: PCIManager.responseIdentifier,
|
|
426
|
+
method: 'mousedownSelectorResponse',
|
|
427
|
+
messageId: params.messageId,
|
|
428
|
+
success
|
|
429
|
+
}, '*');
|
|
430
|
+
} catch (error) {
|
|
431
|
+
console.error('Error performing mousedown on selector:', error);
|
|
432
|
+
window.parent.postMessage({
|
|
433
|
+
source: 'qti-pci-iframe',
|
|
434
|
+
responseIdentifier: PCIManager.responseIdentifier,
|
|
435
|
+
method: 'mousedownSelectorResponse',
|
|
436
|
+
messageId: params.messageId,
|
|
437
|
+
success: false,
|
|
438
|
+
error: error.toString()
|
|
439
|
+
}, '*');
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// Helper function for setting values on elements
|
|
444
|
+
function setValueOnElement(element, value) {
|
|
445
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
446
|
+
element.value = value;
|
|
447
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
448
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
449
|
+
return true;
|
|
450
|
+
} else if (element.tagName === 'SELECT') {
|
|
451
|
+
element.value = value;
|
|
452
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
453
|
+
return true;
|
|
454
|
+
} else if (element.isContentEditable) {
|
|
455
|
+
element.textContent = value;
|
|
456
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Add test-related message handlers (do not override global handlers)
|
|
463
|
+
let expectedParentOriginTest = null;
|
|
464
|
+
window.addEventListener(
|
|
465
|
+
'message',
|
|
466
|
+
function(event) {
|
|
467
|
+
const { data } = event;
|
|
468
|
+
|
|
469
|
+
if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') return;
|
|
470
|
+
if (expectedParentOriginTest === null) expectedParentOriginTest = event.origin;
|
|
471
|
+
else if (event.origin !== expectedParentOriginTest) return;
|
|
472
|
+
if (data.responseIdentifier && data.responseIdentifier !== PCIManager.responseIdentifier) return;
|
|
473
|
+
|
|
474
|
+
switch (data.method) {
|
|
475
|
+
case 'setValueElementByText':
|
|
476
|
+
PCIManager.setValueElementByText(data.params);
|
|
477
|
+
return;
|
|
478
|
+
case 'mousedownOnSelector':
|
|
479
|
+
PCIManager.mousedownOnSelector(data.params);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
true
|
|
484
|
+
);`;
|
|
485
|
+
// Find the position to insert our test handlers
|
|
486
|
+
const insertPosition = originalContent.lastIndexOf('</script>');
|
|
487
|
+
// Insert our handlers
|
|
488
|
+
const modifiedContent = originalContent.substring(0, insertPosition) + testHandlers + originalContent.substring(insertPosition);
|
|
489
|
+
return modifiedContent;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=qti-portable-custom-test-interaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qti-portable-custom-test-interaction.js","sourceRoot":"","sources":["../src/qti-portable-custom-test-interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';\n\nimport type { ItemContext } from '@qti-components/base';\n\n/**\n * Test-specific extension of QtiPortableCustomInteraction that adds methods\n * for interacting with the iframe content in tests.\n */\nexport class QtiPortableCustomInteractionTest extends QtiPortableCustomInteraction {\n #recreatingIframe = false;\n\n /**\n * Gets the HTML content of the iframe for testing purposes\n * @returns Promise that resolves with the HTML content string\n */\n async getIFrameContent(): Promise<string> {\n return new Promise(resolve => {\n const messageId = `get-content-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'getContentResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.content);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('getContent', { messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve('');\n }, 5000);\n });\n }\n\n // Expose the protected context\n public setTestContext(newContext: ItemContext): void {\n this.context = newContext;\n\n // If needed, force an update to push the context changes to the iframe\n this.updateComplete.then(() => {\n // const responseVal = this.responseVariablesToQtiVariableJSON(\n // (newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.value as string | string[]) ||\n // null,\n // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.cardinality || 'single',\n // newContext?.variables?.find(v => v.identifier === this.responseIdentifier)?.baseType || 'string'\n // );\n this.value = newContext?.variables?.find(v => v.identifier === this.responseIdentifier).value as string;\n // this.recreateIframe();\n });\n }\n\n /**\n * Recreates the iframe completely\n * @returns Promise that resolves when the iframe is loaded\n */\n public async recreateIframe(): Promise<void> {\n this.#recreatingIframe = true;\n\n try {\n // Remove existing iframe if it exists\n if (this.iframe) {\n this._iframeLoaded = false;\n this.iframe.remove();\n }\n\n this._pendingMessages = [];\n\n // Create a new iframe\n this.createIframe();\n\n // Wait for iframe to load\n return new Promise<void>(resolve => {\n const loadHandler = () => {\n this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n resolve();\n };\n\n this.addEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n\n // Add timeout to avoid hanging if the event never fires\n setTimeout(() => {\n this.removeEventListener('qti-portable-custom-interaction-loaded', loadHandler);\n resolve();\n }, 5000);\n });\n } finally {\n this.#recreatingIframe = false;\n }\n }\n\n /**\n * Override the original disconnectedCallback to handle iframe recreation\n */\n override disconnectedCallback(): void {\n // Only call super if we're not in the process of recreating the iframe\n // This prevents removing event listeners that we still need\n if (!this.#recreatingIframe) {\n super.disconnectedCallback();\n }\n }\n\n /**\n * Simulates a mouse click at specific coordinates within the iframe\n * @param x The x coordinate\n * @param y The y coordinate\n * @returns Promise that resolves when the click is performed\n */\n async iFrameMouseClick(x: number, y: number): Promise<void> {\n return new Promise(resolve => {\n const messageId = `click-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve();\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('simulateClick', { x, y, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve();\n }, 5000);\n });\n }\n\n /**\n * Clicks on an element identified by a CSS selector\n * @param selector The CSS selector string\n * @returns Promise that resolves with boolean indicating if the element was found and clicked\n */\n async iFrameClickOnElement(selector: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `click-selector-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickSelectorResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('clickOnSelector', { selector, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Clicks on an element containing the specified text\n * @param text The text to search for within elements\n * @returns Promise that resolves with boolean indicating if the element was found and clicked\n */\n async iFrameClickOnElementByText(text: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `click-text-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'clickTextResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('clickOnElementByText', { text, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Sets the value of an input element identified by a CSS selector\n * @param selector The CSS selector string\n * @param value The value to set\n * @returns Promise that resolves with boolean indicating if the element was found and value was set\n */\n async iFrameSetValueElement(selector: string, value: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `set-value-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'setValueResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('setValueElement', { selector, value, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Gets the bounding client rect for an element inside the iframe\n * @param selector The CSS selector string\n * @returns Promise that resolves with the rect or null\n */\n async iFrameGetBoundingClientRect(\n selector: string\n ): Promise<{ left: number; top: number; width: number; height: number } | null> {\n return new Promise(resolve => {\n const messageId = `get-bounding-rect-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'getBoundingRectResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.rect ?? null);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('getBoundingRect', { selector, messageId });\n\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(null);\n }, 5000);\n });\n }\n\n /**\n * Sets the value of an input element containing the specified text\n * @param text The text to search for within elements\n * @param value The value to set\n * @returns Promise that resolves with boolean indicating if the element was found and value was set\n */\n async iFrameSetValueElementByText(text: string, value: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `set-value-text-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'setValueByTextResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('setValueElementByText', { text, value, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Performs a mousedown event on an element identified by a CSS selector\n * @param selector The CSS selector string\n * @returns Promise that resolves with boolean indicating if the element was found and mousedown was performed\n */\n async iFrameMousedownOnElement(selector: string): Promise<boolean> {\n return new Promise(resolve => {\n const messageId = `mousedown-selector-${Date.now()}`;\n\n const messageHandler = (event: MessageEvent) => {\n const { data } = event;\n if (\n data?.source === 'qti-pci-iframe' &&\n (!data?.responseIdentifier || data?.responseIdentifier === this.responseIdentifier) &&\n data?.method === 'mousedownSelectorResponse' &&\n data?.messageId === messageId\n ) {\n window.removeEventListener('message', messageHandler);\n resolve(data.success);\n }\n };\n\n window.addEventListener('message', messageHandler);\n\n this.sendMessageToIframe('mousedownOnSelector', { selector, messageId });\n\n // Set timeout to avoid hanging promises\n setTimeout(() => {\n window.removeEventListener('message', messageHandler);\n resolve(false);\n }, 5000);\n });\n }\n\n /**\n * Override the original generateIframeContent method to include\n * the test message handlers in the iframe HTML\n */\n override generateIframeContent(): string {\n // Get the original iframe HTML from the parent class\n const originalContent = super.generateIframeContent();\n\n // Insert our test message handlers before the closing </script> tag\n const testHandlers = `\n // Test helper functions for QtiPortableCustomInteractionTest\n // Base iframe implementation already provides PCIManager.getContent\n \n // Base iframe implementation already provides PCIManager.simulateClick\n \n // Base iframe implementation already provides PCIManager.clickOnSelector\n \n // Base iframe implementation already provides PCIManager.clickOnElementByText\n \n // Base iframe implementation already provides PCIManager.setValueElement\n \n function deepQuerySelector(root, selector) {\n if (!root) return null;\n try {\n const direct = root.querySelector ? root.querySelector(selector) : null;\n if (direct) return direct;\n } catch (e) {\n // ignore invalid selector for this root\n }\n if (!root.querySelectorAll) return null;\n const nodes = root.querySelectorAll('*');\n for (const node of nodes) {\n if (node && node.shadowRoot) {\n const found = deepQuerySelector(node.shadowRoot, selector);\n if (found) return found;\n }\n }\n return null;\n }\n\n function deepFindElementByExactText(root, text) {\n if (!root || !text) return null;\n if (root.querySelectorAll) {\n const nodes = root.querySelectorAll('*');\n for (const node of nodes) {\n if ((node.textContent || '').trim() === text) return node;\n if (node.shadowRoot) {\n const found = deepFindElementByExactText(node.shadowRoot, text);\n if (found) return found;\n }\n }\n }\n return null;\n }\n\n function escapeSelectorId(id) {\n try {\n if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(id);\n } catch (e) {\n // ignore\n }\n return String(id).replace(/([ #;?%&,.+*~\\\\':\"!^$[\\\\]()=>|\\\\/])/g, '\\\\\\\\$1');\n }\n\n PCIManager.setValueElementByText = function(params) {\n const messageId = params && params.messageId;\n try {\n const text = params && params.text;\n const value = params && params.value;\n\n const textEl = text ? deepFindElementByExactText(document, text) : null;\n let target = null;\n\n if (textEl) {\n const tag = (textEl.tagName || '').toUpperCase();\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {\n target = textEl;\n } else if (tag === 'LABEL' && textEl.htmlFor) {\n const id = escapeSelectorId(textEl.htmlFor);\n target = deepQuerySelector(document, '#' + id);\n }\n\n if (!target) {\n let parent = textEl.parentElement;\n while (parent) {\n const candidate = parent.querySelector ? parent.querySelector('input, textarea, select') : null;\n if (candidate) {\n target = candidate;\n break;\n }\n parent = parent.parentElement;\n }\n }\n }\n\n const success = !!target && setValueOnElement(target, value);\n window.parent.postMessage(\n {\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'setValueByTextResponse',\n messageId: messageId,\n success: success\n },\n '*'\n );\n } catch (error) {\n console.error('Error setting value by text:', error);\n window.parent.postMessage(\n {\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'setValueByTextResponse',\n messageId: messageId,\n success: false,\n error: error.toString()\n },\n '*'\n );\n }\n };\n \n PCIManager.mousedownOnSelector = function(params) {\n try {\n const element = params && params.selector ? deepQuerySelector(document, params.selector) : null;\n \n let success = false;\n \n if (element) {\n const rect = element.getBoundingClientRect();\n const mousedownEvent = new MouseEvent('mousedown', {\n bubbles: true,\n cancelable: true,\n view: window,\n clientX: rect.left + rect.width / 2,\n clientY: rect.top + rect.height / 2\n });\n element.dispatchEvent(mousedownEvent);\n success = true;\n }\n \n window.parent.postMessage({\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'mousedownSelectorResponse',\n messageId: params.messageId,\n success\n }, '*');\n } catch (error) {\n console.error('Error performing mousedown on selector:', error);\n window.parent.postMessage({\n source: 'qti-pci-iframe',\n responseIdentifier: PCIManager.responseIdentifier,\n method: 'mousedownSelectorResponse',\n messageId: params.messageId,\n success: false,\n error: error.toString()\n }, '*');\n }\n };\n \n // Helper function for setting values on elements\n function setValueOnElement(element, value) {\n if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {\n element.value = value;\n element.dispatchEvent(new Event('input', { bubbles: true }));\n element.dispatchEvent(new Event('change', { bubbles: true }));\n return true;\n } else if (element.tagName === 'SELECT') {\n element.value = value;\n element.dispatchEvent(new Event('change', { bubbles: true }));\n return true;\n } else if (element.isContentEditable) {\n element.textContent = value;\n element.dispatchEvent(new Event('input', { bubbles: true }));\n return true;\n }\n return false;\n }\n \n // Add test-related message handlers (do not override global handlers)\n let expectedParentOriginTest = null;\n window.addEventListener(\n 'message',\n function(event) {\n const { data } = event;\n\n if (event.source !== window.parent || !data || data.source !== 'qti-portable-custom-interaction') return;\n if (expectedParentOriginTest === null) expectedParentOriginTest = event.origin;\n else if (event.origin !== expectedParentOriginTest) return;\n if (data.responseIdentifier && data.responseIdentifier !== PCIManager.responseIdentifier) return;\n\n switch (data.method) {\n case 'setValueElementByText':\n PCIManager.setValueElementByText(data.params);\n return;\n case 'mousedownOnSelector':\n PCIManager.mousedownOnSelector(data.params);\n return;\n }\n },\n true\n );`;\n\n // Find the position to insert our test handlers\n const insertPosition = originalContent.lastIndexOf('</script>');\n\n // Insert our handlers\n const modifiedContent =\n originalContent.substring(0, insertPosition) + testHandlers + originalContent.substring(insertPosition);\n\n return modifiedContent;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'qti-portable-custom-interaction-test': QtiPortableCustomInteractionTest;\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';
|
|
2
|
+
import { QtiPortableCustomInteractionTest } from './qti-portable-custom-test-interaction';
|
|
3
|
+
customElements.define('qti-portable-custom-interaction', QtiPortableCustomInteraction);
|
|
4
|
+
customElements.define('qti-portable-custom-interaction-test', QtiPortableCustomInteractionTest);
|
|
5
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../src/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAE1F,cAAc,CAAC,MAAM,CAAC,iCAAiC,EAAE,4BAA4B,CAAC,CAAC;AACvF,cAAc,CAAC,MAAM,CAAC,sCAAsC,EAAE,gCAAgC,CAAC,CAAC","sourcesContent":["import { QtiPortableCustomInteraction } from './qti-portable-custom-interaction';\nimport { QtiPortableCustomInteractionTest } from './qti-portable-custom-test-interaction';\n\ncustomElements.define('qti-portable-custom-interaction', QtiPortableCustomInteraction);\ncustomElements.define('qti-portable-custom-interaction-test', QtiPortableCustomInteractionTest);\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qti-components/portable-custom-interaction",
|
|
3
|
+
"description": "QTI portable-custom-interaction interaction",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"author": "",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@qti-components/utilities": "^1.3.1",
|
|
8
|
+
"@qti-components/interactions-core": "^1.0.1",
|
|
9
|
+
"@qti-components/base": "^1.4.1"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@lit/context": "^1.1.6",
|
|
13
|
+
"lit": "^3.3.1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"interactions",
|
|
17
|
+
"qti",
|
|
18
|
+
"portable-custom-interaction"
|
|
19
|
+
],
|
|
20
|
+
"license": "GPL-3.0-only",
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@lit/context": "^1.1.6",
|
|
24
|
+
"lit": "^3.3.1"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": true,
|
|
30
|
+
"type": "module",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./register": {
|
|
41
|
+
"types": "./dist/register.d.ts",
|
|
42
|
+
"import": "./dist/register.js"
|
|
43
|
+
},
|
|
44
|
+
"./*": "./dist/*",
|
|
45
|
+
"./styles": {
|
|
46
|
+
"types": "./dist/qti-portable-custom-interaction.styles.d.ts",
|
|
47
|
+
"import": "./dist/qti-portable-custom-interaction.styles.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/Citolab/qti-components.git",
|
|
53
|
+
"directory": "packages/interactions/portable-custom-interaction"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"attw": "attw --profile esm-only --pack",
|
|
57
|
+
"build": "tsc --paths null",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"release": "semantic-release --no-ci",
|
|
60
|
+
"release:semantic": "semantic-release --no-ci",
|
|
61
|
+
"release:semantic:dry-run": "semantic-release --dry-run --no-ci"
|
|
62
|
+
}
|
|
63
|
+
}
|