@vue-skuilder/common-ui 0.1.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.
Files changed (68) hide show
  1. package/dist/assets/index.css +10 -0
  2. package/dist/common-ui.es.js +16404 -0
  3. package/dist/common-ui.es.js.map +1 -0
  4. package/dist/common-ui.umd.js +9 -0
  5. package/dist/common-ui.umd.js.map +1 -0
  6. package/dist/components/HeatMap.types.d.ts +13 -0
  7. package/dist/components/PaginatingToolbar.types.d.ts +40 -0
  8. package/dist/components/SkMouseTrap.types.d.ts +3 -0
  9. package/dist/components/SkMouseTrapToolTip.types.d.ts +35 -0
  10. package/dist/components/SnackbarService.d.ts +11 -0
  11. package/dist/components/StudySession.types.d.ts +6 -0
  12. package/dist/components/auth/index.d.ts +4 -0
  13. package/dist/components/cardRendering/MarkdownRendererHelpers.d.ts +22 -0
  14. package/dist/components/studentInputs/BaseUserInput.d.ts +16 -0
  15. package/dist/components/studentInputs/RadioMultipleChoice.types.d.ts +5 -0
  16. package/dist/composables/CompositionViewable.d.ts +33 -0
  17. package/dist/composables/Displayable.d.ts +47 -0
  18. package/dist/composables/index.d.ts +2 -0
  19. package/dist/index.d.ts +36 -0
  20. package/dist/plugins/pinia.d.ts +5 -0
  21. package/dist/stores/useAuthStore.d.ts +225 -0
  22. package/dist/stores/useCardPreviewModeStore.d.ts +8 -0
  23. package/dist/stores/useConfigStore.d.ts +11 -0
  24. package/dist/utils/SkldrMouseTrap.d.ts +32 -0
  25. package/package.json +67 -0
  26. package/src/components/HeatMap.types.ts +15 -0
  27. package/src/components/HeatMap.vue +354 -0
  28. package/src/components/PaginatingToolbar.types.ts +48 -0
  29. package/src/components/PaginatingToolbar.vue +75 -0
  30. package/src/components/SkMouseTrap.types.ts +3 -0
  31. package/src/components/SkMouseTrap.vue +70 -0
  32. package/src/components/SkMouseTrapToolTip.types.ts +41 -0
  33. package/src/components/SkMouseTrapToolTip.vue +316 -0
  34. package/src/components/SnackbarService.ts +39 -0
  35. package/src/components/SnackbarService.vue +71 -0
  36. package/src/components/StudySession.types.ts +6 -0
  37. package/src/components/StudySession.vue +670 -0
  38. package/src/components/StudySessionTimer.vue +121 -0
  39. package/src/components/auth/UserChip.vue +106 -0
  40. package/src/components/auth/UserLogin.vue +141 -0
  41. package/src/components/auth/UserLoginAndRegistrationContainer.vue +85 -0
  42. package/src/components/auth/UserRegistration.vue +181 -0
  43. package/src/components/auth/index.ts +4 -0
  44. package/src/components/cardRendering/AudioAutoPlayer.vue +131 -0
  45. package/src/components/cardRendering/CardLoader.vue +123 -0
  46. package/src/components/cardRendering/CardViewer.vue +101 -0
  47. package/src/components/cardRendering/CodeBlockRenderer.vue +81 -0
  48. package/src/components/cardRendering/MarkdownRenderer.vue +46 -0
  49. package/src/components/cardRendering/MarkdownRendererHelpers.ts +114 -0
  50. package/src/components/cardRendering/MdTokenRenderer.vue +244 -0
  51. package/src/components/studentInputs/BaseUserInput.ts +71 -0
  52. package/src/components/studentInputs/MultipleChoiceOption.vue +127 -0
  53. package/src/components/studentInputs/RadioMultipleChoice.types.ts +6 -0
  54. package/src/components/studentInputs/RadioMultipleChoice.vue +168 -0
  55. package/src/components/studentInputs/TrueFalse.vue +27 -0
  56. package/src/components/studentInputs/UserInputNumber.vue +63 -0
  57. package/src/components/studentInputs/UserInputString.vue +89 -0
  58. package/src/components/studentInputs/fillInInput.vue +71 -0
  59. package/src/composables/CompositionViewable.ts +180 -0
  60. package/src/composables/Displayable.ts +133 -0
  61. package/src/composables/index.ts +2 -0
  62. package/src/index.ts +79 -0
  63. package/src/plugins/pinia.ts +24 -0
  64. package/src/stores/useAuthStore.ts +92 -0
  65. package/src/stores/useCardPreviewModeStore.ts +32 -0
  66. package/src/stores/useConfigStore.ts +60 -0
  67. package/src/utils/SkldrMouseTrap.ts +141 -0
  68. package/src/vue-shims.d.ts +5 -0
@@ -0,0 +1,244 @@
1
+ <template>
2
+ <span v-if="isText(token)">
3
+ <span v-if="!token.tokens || token.tokens.length === 0">
4
+ <span v-if="isComponent(token)">
5
+ <!-- <component :is="parsedComponent(token).is" v-if="!last" :text="parsedComponent(token).text" /> -->
6
+ <component :is="getComponent(parsedComponent(token).is)" v-if="!last" :text="parsedComponent(token).text" />
7
+ </span>
8
+ <span v-else-if="containsComponent(token)">
9
+ <md-token-renderer v-for="(subTok, j) in splitTextToken(token)" :key="j" :token="subTok" />
10
+ </span>
11
+ <span v-else>{{ decodeBasicEntities(token.text) }}</span>
12
+ </span>
13
+ <span v-else-if="token.tokens && token.tokens.length !== 0">
14
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
15
+ </span>
16
+ </span>
17
+
18
+ <span v-else-if="token.type === 'heading'">
19
+ <h1 v-if="token.depth === 1" class="text-h2">
20
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
21
+ </h1>
22
+ <h2 v-else-if="token.depth === 2" class="text-h3">
23
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
24
+ </h2>
25
+ <h3 v-else-if="token.depth === 3" class="text-h4">
26
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
27
+ </h3>
28
+ <h4 v-else-if="token.depth === 4">
29
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
30
+ </h4>
31
+ <h5 v-else-if="token.depth === 5">
32
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
33
+ </h5>
34
+ <h6 v-else-if="token.depth === 6">
35
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
36
+ </h6>
37
+ </span>
38
+
39
+ <strong v-else-if="token.type === 'strong'">
40
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
41
+ </strong>
42
+
43
+ <p v-else-if="token.type === 'paragraph'" class="text-h5">
44
+ <span v-if="containsComponent(token)">
45
+ <md-token-renderer
46
+ v-for="(splitTok, j) in splitParagraphToken(token)"
47
+ :key="j"
48
+ :token="splitTok"
49
+ :last="last && token.tokens.length === 1 && j === splitParagraphToken(token).length - 1"
50
+ />
51
+ </span>
52
+ <template v-else>
53
+ <md-token-renderer
54
+ v-for="(subTok, j) in token.tokens"
55
+ :key="j"
56
+ :token="subTok"
57
+ :last="last && token.tokens.length === 1"
58
+ />
59
+ </template>
60
+ </p>
61
+
62
+ <a v-else-if="token.type === 'link'" :href="token.href" :title="token.title">
63
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
64
+ </a>
65
+
66
+ <ul v-else-if="token.type === 'list' && token.ordered === false">
67
+ <md-token-renderer v-for="(item, j) in token.items" :key="j" :token="item" />
68
+ </ul>
69
+
70
+ <ol v-else-if="token.type === 'list' && token.ordered === true">
71
+ <md-token-renderer v-for="(item, j) in token.items" :key="j" :token="item" />
72
+ </ol>
73
+
74
+ <li v-else-if="token.type === 'list_item'">
75
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
76
+ </li>
77
+
78
+ <img v-else-if="token.type === 'image'" :src="token.href" :alt="token.title" />
79
+ <hr v-else-if="token.type === 'hr'" />
80
+ <br v-else-if="token.type === 'br'" />
81
+ <del v-else-if="token.type === 'del'" />
82
+
83
+ <table v-else-if="token.type === 'table'" :align="token.align">
84
+ <thead>
85
+ <th v-for="(h, j) in token.header" :key="j">{{ h.text }}</th>
86
+ </thead>
87
+ <tbody>
88
+ <tr v-for="(row, r) in token.rows" :key="r">
89
+ <td v-for="(cell, c) in row" :key="c">{{ cell.text }}</td>
90
+ </tr>
91
+ </tbody>
92
+ </table>
93
+
94
+ <span v-else-if="token.type === 'html'" v-html="token.raw"></span>
95
+
96
+ <code-block-renderer v-else-if="token.type === 'code'" :code="token.text" :language="token.lang" />
97
+ <!-- <highlightjs v-else-if="token.type === 'code'" class="hljs_render pa-2" :language="token.lang" :code="token.text" /> -->
98
+
99
+ <code v-else-if="token.type === 'codespan'" class="codespan" v-html="token.text"></code>
100
+
101
+ <blockquote v-else-if="token.type === 'blockquote'">
102
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
103
+ </blockquote>
104
+
105
+ <span v-else-if="token.type === 'escape'">{{ token.text }}</span>
106
+
107
+ <em v-else-if="token.type === 'em'">
108
+ <md-token-renderer v-for="(subTok, j) in token.tokens" :key="j" :token="subTok" />
109
+ </em>
110
+ </template>
111
+
112
+ <script setup lang="ts">
113
+ import {
114
+ containsComponent as _containsComponent,
115
+ isComponent as _isComponent,
116
+ splitParagraphToken as _splitParagraphToken,
117
+ splitTextToken as _splitTextToken,
118
+ TokenOrComponent,
119
+ } from './MarkdownRendererHelpers';
120
+ import CodeBlockRenderer from './CodeBlockRenderer.vue';
121
+ import FillInInput from '../../components/studentInputs/fillInInput.vue';
122
+ import { MarkedToken, Tokens } from 'marked';
123
+ import { markRaw } from 'vue';
124
+ import { PropType } from 'vue';
125
+
126
+ // Register components to be used in the template
127
+ // In Vue 3 with <script setup>, components used via :is must be explicitly registered
128
+ const components = {
129
+ fillIn: markRaw(FillInInput),
130
+ // Add other dynamic components here
131
+ };
132
+
133
+ // Define component props
134
+ defineProps({
135
+ token: {
136
+ type: Object as PropType<TokenOrComponent>,
137
+ required: true,
138
+ },
139
+ last: {
140
+ type: Boolean,
141
+ required: false,
142
+ default: false,
143
+ },
144
+ });
145
+
146
+ // Methods
147
+ function isComponent(token: MarkedToken): boolean {
148
+ const result = _isComponent(token);
149
+ return result;
150
+ }
151
+
152
+ function containsComponent(token: MarkedToken): boolean {
153
+ const result = _containsComponent(token);
154
+ return result;
155
+ }
156
+
157
+ function splitTextToken(token: MarkedToken): Tokens.Text[] {
158
+ return _splitTextToken(token);
159
+ }
160
+
161
+ function splitParagraphToken(token: Tokens.Paragraph): TokenOrComponent[] {
162
+ return _splitParagraphToken(token);
163
+ }
164
+
165
+ function parsedComponent(token: MarkedToken): {
166
+ is: string;
167
+ text: string;
168
+ } {
169
+ // [ ] switching on component types & loading custom component
170
+ //
171
+ // sketch:
172
+ // const demoustached = token.text.slice(2, token.text.length - 2);
173
+ // const firstToken = demoustached.split(' ')[0];
174
+ // if (firstToken.charAt(firstToken.length - 1) == '>') {
175
+ // return {
176
+ // is: firstToken.slice(0, firstToken.length - 1),
177
+ // text: demoustached.slice(firstToken.length + 1, demoustached.length),
178
+ // };
179
+ // }
180
+
181
+ let text = '';
182
+ if ('text' in token && typeof token.text === 'string') {
183
+ text = token.text;
184
+ } else if ('raw' in token && typeof token.raw === 'string') {
185
+ text = token.raw;
186
+ }
187
+
188
+ // This now returns a component from our registered components object
189
+ return {
190
+ is: 'fillIn', // This should match a key in the components object
191
+ text,
192
+ };
193
+ }
194
+
195
+ function getComponent(componentName: string) {
196
+ // Return the component instance from our components object
197
+ return components[componentName as keyof typeof components];
198
+ }
199
+
200
+ function decodeBasicEntities(text: string): string {
201
+ return text
202
+ .replace(/&#39;/g, "'")
203
+ .replace(/&quot;/g, '"')
204
+ .replace(/&amp;/g, '&')
205
+ .replace(/&lt;/g, '<')
206
+ .replace(/&gt;/g, '>');
207
+ }
208
+
209
+ function isText(tok: TokenOrComponent): boolean {
210
+ return (tok as any).inLink === undefined && tok.type === 'text';
211
+ }
212
+
213
+ // Make these functions and objects available to the template
214
+ defineExpose({
215
+ isComponent,
216
+ containsComponent,
217
+ splitTextToken,
218
+ splitParagraphToken,
219
+ parsedComponent,
220
+ decodeBasicEntities,
221
+ isText,
222
+ components,
223
+ getComponent,
224
+ });
225
+ </script>
226
+
227
+ <style lang="css" scoped>
228
+ blockquote {
229
+ border-left: 3px teal solid;
230
+ padding-left: 8px;
231
+ }
232
+
233
+ .codespan {
234
+ padding-left: 3px;
235
+ padding-right: 3px;
236
+ margin-left: 1px;
237
+ margin-right: 1px;
238
+ }
239
+
240
+ p {
241
+ margin-bottom: 15px;
242
+ margin-top: 15px;
243
+ }
244
+ </style>
@@ -0,0 +1,71 @@
1
+ import { defineComponent } from 'vue';
2
+ import { Answer, log } from '@vue-skuilder/common';
3
+ import { useCardPreviewModeStore } from '../../stores/useCardPreviewModeStore';
4
+ import { ViewComponent } from '../../composables/Displayable';
5
+ import { isQuestionView } from '../../composables/CompositionViewable';
6
+ import { QuestionRecord } from '@vue-skuilder/db';
7
+ import { Store } from 'pinia';
8
+
9
+ export default defineComponent({
10
+ name: 'UserInput',
11
+ data() {
12
+ return {
13
+ answer: '' as Answer,
14
+ previewModeStore: null as ReturnType<typeof useCardPreviewModeStore> | null,
15
+ };
16
+ },
17
+ mounted() {
18
+ // This happens after Pinia is initialized but before the component is used
19
+ this.previewModeStore = useCardPreviewModeStore();
20
+ },
21
+ computed: {
22
+ autofocus(): boolean {
23
+ return !this.previewModeStore?.previewMode;
24
+ },
25
+ autoFocus(): boolean {
26
+ return this.autofocus;
27
+ },
28
+ },
29
+ methods: {
30
+ submitAnswer(answer: Answer): QuestionRecord {
31
+ return this.submit(answer);
32
+ },
33
+ // isQuestionView(a: any): a is QuestionView<Question> {
34
+ // return (a as QuestionView<Question>).submitAnswer !== undefined;
35
+ // },
36
+ submit(answer: Answer) {
37
+ return this.getQuestionViewAncestor().submitAnswer(answer, this.$options.name ?? 'UserInput');
38
+ },
39
+ getQuestionViewAncestor(): ViewComponent {
40
+ let ancestor = this.$parent;
41
+ let count = 0;
42
+
43
+ while (ancestor && !isQuestionView(ancestor)) {
44
+ const nextAncestor = ancestor.$parent;
45
+ if (!nextAncestor) {
46
+ const err = `
47
+ UserInput.submit() has failed.
48
+ The input element has no QuestionView ancestor element.`;
49
+ log(err);
50
+ throw new Error(err);
51
+ }
52
+ ancestor = nextAncestor;
53
+ count++;
54
+
55
+ if (count > 100) {
56
+ const err = `
57
+ UserInput.submit() has failed.
58
+ Exceeded maximum ancestor lookup depth.`;
59
+ log(err);
60
+ throw new Error(err);
61
+ }
62
+ }
63
+
64
+ if (!ancestor) {
65
+ throw new Error('No QuestionView ancestor found');
66
+ }
67
+
68
+ return ancestor;
69
+ },
70
+ },
71
+ });
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <v-card :class="`${className}`" @mouseover="select" @click="submitThisOption">
3
+ <markdown-renderer :md="content" />
4
+ </v-card>
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ import { defineComponent, defineAsyncComponent, PropType } from 'vue';
9
+
10
+ export default defineComponent({
11
+ name: 'MultipleChoiceOption',
12
+
13
+ components: {
14
+ MarkdownRenderer: defineAsyncComponent(() => import('../../components/cardRendering/MarkdownRenderer.vue')),
15
+ },
16
+
17
+ props: {
18
+ content: {
19
+ type: String,
20
+ required: true,
21
+ },
22
+ selected: {
23
+ type: Boolean,
24
+ required: true,
25
+ },
26
+ number: {
27
+ type: Number,
28
+ required: true,
29
+ },
30
+ setSelection: {
31
+ type: Function as PropType<(selection: number) => void>,
32
+ required: true,
33
+ },
34
+ submit: {
35
+ type: Function as PropType<() => void>,
36
+ required: true,
37
+ },
38
+ markedWrong: {
39
+ type: Boolean,
40
+ required: true,
41
+ },
42
+ },
43
+
44
+ computed: {
45
+ className(): string {
46
+ let color: string;
47
+
48
+ switch (this.number) {
49
+ case 0:
50
+ color = 'bg-red';
51
+ break;
52
+ case 1:
53
+ color = 'bg-purple';
54
+ break;
55
+ case 2:
56
+ color = 'bg-indigo';
57
+ break;
58
+ case 3:
59
+ color = 'bg-light-blue';
60
+ break;
61
+ case 4:
62
+ color = 'bg-teal';
63
+ break;
64
+ case 5:
65
+ color = 'bg-deep-orange';
66
+ break;
67
+ default:
68
+ color = 'bg-grey';
69
+ break;
70
+ }
71
+
72
+ if (this.selected && !this.markedWrong) {
73
+ return `choice selected ${color} lighten-3 elevation-8`;
74
+ } else if (!this.selected && !this.markedWrong) {
75
+ return `choice not-selected ${color} lighten-4 elevation-1`;
76
+ } else if (this.selected && this.markedWrong) {
77
+ return `choice selected grey lighten-2 elevation-8`;
78
+ } else if (!this.selected && this.markedWrong) {
79
+ return 'choice not-selected grey lighten-2 elevation-0';
80
+ } else {
81
+ throw new Error(
82
+ `'selected' and 'markedWrong' props in MultipleChoiceOption are in an impossible configuration.`
83
+ );
84
+ }
85
+ },
86
+ },
87
+
88
+ methods: {
89
+ select(): void {
90
+ this.setSelection(this.number);
91
+ },
92
+
93
+ submitThisOption(): void {
94
+ if (this.markedWrong) {
95
+ return;
96
+ } else {
97
+ this.select();
98
+ this.submit();
99
+ }
100
+ },
101
+ },
102
+ });
103
+ </script>
104
+
105
+ <style scoped>
106
+ .choice {
107
+ text-align: center;
108
+ display: inline-block;
109
+ border-radius: 4px;
110
+ padding-left: 15px;
111
+ padding-right: 15px;
112
+ padding-top: 5px;
113
+ padding-bottom: 5px;
114
+ margin: 10px;
115
+ min-width: 75px; /* prevent tiny click-btns on, eg, one-letter answers */
116
+ transition: all 0.2s ease-in-out;
117
+ }
118
+
119
+ .selected {
120
+ transform: translateY(-10px) /* rotate(3deg) */ scale(1.15);
121
+ z-index: 1;
122
+ }
123
+
124
+ .not-selected {
125
+ z-index: 0;
126
+ }
127
+ </style>
@@ -0,0 +1,6 @@
1
+ import { Answer } from '@vue-skuilder/common';
2
+
3
+ export interface RadioMultipleChoiceAnswer extends Answer {
4
+ choiceList: string[];
5
+ selection: number;
6
+ }
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <div ref="containerRef" class="multipleChoice">
3
+ <MultipleChoiceOption
4
+ v-for="(choice, i) in choiceList"
5
+ :key="i"
6
+ :content="choice"
7
+ :selected="choiceList.indexOf(choice) === currentSelection"
8
+ :number="choiceList.indexOf(choice)"
9
+ :set-selection="setSelection"
10
+ :submit="forwardSelection"
11
+ :marked-wrong="choiceIsWrong(choice)"
12
+ />
13
+ </div>
14
+ </template>
15
+
16
+ <script lang="ts">
17
+ import UserInput from './BaseUserInput';
18
+ import MultipleChoiceOption from './MultipleChoiceOption.vue';
19
+ import { defineComponent, PropType } from 'vue';
20
+ import { SkldrMouseTrap } from '../../utils/SkldrMouseTrap';
21
+ import { RadioMultipleChoiceAnswer } from './RadioMultipleChoice.types';
22
+
23
+ export default defineComponent({
24
+ name: 'RadioMultipleChoice',
25
+ components: {
26
+ MultipleChoiceOption,
27
+ },
28
+ extends: UserInput,
29
+ props: {
30
+ choiceList: {
31
+ type: Array as PropType<string[]>,
32
+ required: true,
33
+ },
34
+ },
35
+ data() {
36
+ return {
37
+ currentSelection: -1,
38
+ incorrectSelections: [] as number[],
39
+ containerRef: null as null | HTMLElement,
40
+ _registeredHotkeys: [] as (string | string[])[],
41
+ };
42
+ },
43
+ watch: {
44
+ choiceList: {
45
+ immediate: true,
46
+ handler(newList) {
47
+ if (newList?.length) {
48
+ this.bindKeys();
49
+ }
50
+ },
51
+ },
52
+ },
53
+ mounted() {
54
+ if (this.containerRef) {
55
+ this.containerRef.focus();
56
+ }
57
+ },
58
+ unmounted() {
59
+ this.unbindKeys();
60
+ },
61
+ methods: {
62
+ forwardSelection(): void {
63
+ if (this.choiceIsWrong(this.choiceList[this.currentSelection])) {
64
+ return;
65
+ } else if (this.currentSelection !== -1) {
66
+ const ans: RadioMultipleChoiceAnswer = {
67
+ choiceList: this.choiceList,
68
+ selection: this.currentSelection,
69
+ };
70
+ const record = this.submitAnswer(ans);
71
+
72
+ if (!record.isCorrect) {
73
+ this.incorrectSelections.push(this.currentSelection);
74
+ }
75
+ }
76
+ },
77
+ setSelection(selection: number): void {
78
+ if (selection < this.choiceList.length) {
79
+ this.currentSelection = selection;
80
+ }
81
+ },
82
+ incrementSelection(): void {
83
+ if (this.currentSelection === -1) {
84
+ this.currentSelection = Math.ceil(this.choiceList.length / 2);
85
+ } else {
86
+ this.currentSelection = Math.min(this.choiceList.length - 1, this.currentSelection + 1);
87
+ }
88
+ },
89
+ decrementSelection(): void {
90
+ if (this.currentSelection === -1) {
91
+ this.currentSelection = Math.floor(this.choiceList.length / 2 - 1);
92
+ } else {
93
+ this.currentSelection = Math.max(0, this.currentSelection - 1);
94
+ }
95
+ },
96
+ choiceIsWrong(choice: string): boolean {
97
+ let ret = false;
98
+ this.incorrectSelections.forEach((sel) => {
99
+ if (this.choiceList[sel] === choice) {
100
+ ret = true;
101
+ }
102
+ });
103
+ return ret;
104
+ },
105
+ bindKeys() {
106
+ const hotkeys = [
107
+ {
108
+ hotkey: 'left',
109
+ callback: this.decrementSelection,
110
+ command: 'Move selection left',
111
+ },
112
+ {
113
+ hotkey: 'right',
114
+ callback: this.incrementSelection,
115
+ command: 'Move selection right',
116
+ },
117
+ {
118
+ hotkey: 'enter',
119
+ callback: this.forwardSelection,
120
+ command: 'Submit selection',
121
+ },
122
+ // Add number key bindings
123
+ ...Array.from({ length: this.choiceList.length }, (_, i) => ({
124
+ hotkey: (i + 1).toString(),
125
+ callback: () => this.setSelection(i),
126
+ command: `Select ${((i) => {
127
+ switch (i) {
128
+ case 0:
129
+ return 'first';
130
+ case 1:
131
+ return 'second';
132
+ case 2:
133
+ return 'third';
134
+ case 3:
135
+ return 'fourth';
136
+ case 4:
137
+ return 'fifth';
138
+ case 5:
139
+ return 'sixth';
140
+ default:
141
+ return `${i + 1}th`;
142
+ }
143
+ })(i)} option`,
144
+ })),
145
+ ];
146
+
147
+ SkldrMouseTrap.addBinding(hotkeys);
148
+
149
+ // Store hotkeys for cleanup
150
+ this._registeredHotkeys = hotkeys.map(k => k.hotkey);
151
+ },
152
+
153
+ unbindKeys() {
154
+ // Remove specific hotkeys instead of resetting all
155
+ if (this._registeredHotkeys) {
156
+ this._registeredHotkeys.forEach(key => {
157
+ SkldrMouseTrap.removeBinding(key);
158
+ });
159
+ }
160
+ },
161
+ // bindNumberKey(n: number): void {
162
+ // this.MouseTrap.bind(n.toString(), () => {
163
+ // this.currentSelection = n - 1;
164
+ // });
165
+ // },
166
+ },
167
+ });
168
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div data-viewable="TrueFalse">
3
+ <RadioMultipleChoice :choice-list="['True', 'False']" :MouseTrap="MouseTrap" :submit="submit" />
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ import { defineComponent, PropType } from 'vue';
9
+ import RadioMultipleChoice from './RadioMultipleChoice.vue';
10
+
11
+ export default defineComponent({
12
+ name: 'TrueFalse',
13
+ components: {
14
+ RadioMultipleChoice,
15
+ },
16
+ props: {
17
+ MouseTrap: {
18
+ type: Object as PropType<any>,
19
+ required: true,
20
+ },
21
+ submit: {
22
+ type: Function as PropType<(selection: number) => void>,
23
+ required: true,
24
+ },
25
+ },
26
+ });
27
+ </script>