Skip to content

Commit 31a8d05

Browse files
feat: Initial Scaffold for QTI editor components and add a development page
Signed-off-by: Abhishek-Punhani <punhani.manavabhi@gmail.com>
1 parent f91b652 commit 31a8d05

7 files changed

Lines changed: 620 additions & 0 deletions

File tree

contentcuration/contentcuration/frontend/channelEdit/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const RouteNames = {
1818
TRASH: 'TRASH',
1919
ADD_PREVIOUS_STEPS: 'ADD_PREVIOUS_STEPS',
2020
ADD_NEXT_STEPS: 'ADD_NEXT_STEPS',
21+
QTI_DEMO: 'QTI_DEMO',
2122
};
2223

2324
export const viewModes = {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<template>
2+
3+
<div>
4+
<div style="padding: 16px 24px 0">
5+
<div
6+
style="
7+
padding: 16px;
8+
color: #2196f3;
9+
background-color: transparent;
10+
border: 1px solid #2196f3;
11+
border-radius: 4px;
12+
"
13+
>
14+
<strong>QTI Editor — Dev Demo</strong>
15+
&nbsp;Hardcoded items. Changes are local only and not persisted.
16+
</div>
17+
</div>
18+
19+
<QTIEditor
20+
:assessments="assessments"
21+
@update="onUpdate"
22+
/>
23+
</div>
24+
25+
</template>
26+
27+
28+
<script>
29+
30+
import { ref, defineComponent } from 'vue';
31+
import QTIEditor from 'shared/views/QTIEditor/index';
32+
import { QtiInteraction } from 'shared/views/QTIEditor/constants';
33+
34+
/**
35+
* Hardcoded items covering three interaction types so the closed-card
36+
* type label can be visually verified.
37+
*/
38+
const INITIAL_ASSESSMENTS = [
39+
{
40+
id: 'demo-item-1',
41+
type: QtiInteraction.CHOICE,
42+
title: 'Which planet is closest to the Sun?',
43+
},
44+
{
45+
id: 'demo-item-2',
46+
type: QtiInteraction.EXTENDED_TEXT,
47+
title: 'Describe the water cycle in your own words.',
48+
},
49+
{
50+
id: 'demo-item-3',
51+
type: QtiInteraction.ORDER,
52+
title: 'Arrange these events in chronological order.',
53+
},
54+
];
55+
56+
export default defineComponent({
57+
name: 'QTIDemoPage',
58+
59+
components: { QTIEditor },
60+
61+
setup() {
62+
const assessments = ref(INITIAL_ASSESSMENTS);
63+
64+
function onUpdate(newList) {
65+
assessments.value = newList;
66+
}
67+
68+
return { assessments, onUpdate };
69+
},
70+
});
71+
72+
</script>

contentcuration/contentcuration/frontend/channelEdit/router.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import TrashModal from './views/trash/TrashModal';
99
import SearchOrBrowseWindow from './views/ImportFromChannels/SearchOrBrowseWindow';
1010
import ReviewSelectionsPage from './views/ImportFromChannels/ReviewSelectionsPage';
1111
import EditModal from './components/edit/EditModal';
12+
import QTIDemoPage from './pages/QTIDemoPage';
1213
import ChannelDetailsModal from 'shared/views/channel/ChannelDetailsModal';
1314
import ChannelModal from 'shared/views/channel/ChannelModal';
1415
import { RouteNames as ChannelRouteNames } from 'frontend/channelList/constants';
@@ -244,6 +245,11 @@ const router = new VueRouter({
244245
});
245246
},
246247
},
248+
{
249+
name: RouteNames.QTI_DEMO,
250+
path: '/qti-demo',
251+
component: QTIDemoPage,
252+
},
247253
{
248254
name: RouteNames.TREE_VIEW,
249255
path: '/:nodeId/:detailNodeId?',
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<template>
2+
3+
<KPageContainer
4+
noPadding
5+
:topMargin="0"
6+
class="item question-card"
7+
:class="{ closed: !isOpen }"
8+
data-test="item"
9+
@click.native="onCardClick"
10+
>
11+
<div
12+
class="question-card-header"
13+
:style="{ borderBottom: isOpen ? `1px solid ${$themePalette.grey.v_200}` : 'none' }"
14+
>
15+
<h3
16+
class="question-card-title"
17+
:style="{ color: $themePalette.grey.v_800 }"
18+
>
19+
<template v-if="isOpen">
20+
{{ questionNumberLabel }}
21+
</template>
22+
<template v-else>
23+
{{ questionNumberAndTypeLabel }}
24+
</template>
25+
</h3>
26+
27+
<div class="question-card-actions toolbar">
28+
<AssessmentItemToolbar
29+
:iconActionsConfig="iconActionsConfig"
30+
:menuActionsConfig="menuActionsConfig"
31+
:displayMenu="true"
32+
:canMoveUp="canMoveUp"
33+
:canMoveDown="canMoveDown"
34+
:canEdit="!isOpen"
35+
:collapse="windowIsSmall"
36+
:itemLabel="toolbarItemLabel"
37+
analyticsLabel="QTI Question"
38+
data-test="toolbar"
39+
@click="action => $emit('action', action)"
40+
/>
41+
</div>
42+
</div>
43+
44+
<div
45+
v-if="isOpen || displayAnswersPreview"
46+
class="question-card-body"
47+
>
48+
<p :style="{ color: $themePalette.grey.v_500, margin: 0, fontStyle: 'italic' }">
49+
{{ questionContentPlaceholder }}
50+
</p>
51+
</div>
52+
53+
<div
54+
v-if="isOpen"
55+
class="question-card-footer"
56+
>
57+
<KButton
58+
:text="closeBtnLabel"
59+
class="close-item-btn"
60+
data-test="closeBtn"
61+
@click="$emit('close')"
62+
/>
63+
</div>
64+
</KPageContainer>
65+
66+
</template>
67+
68+
69+
<script>
70+
71+
import { computed, defineComponent } from 'vue';
72+
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
73+
import { useQTIStr } from '../../qtiEditorStrings';
74+
import { QtiInteraction } from '../../constants';
75+
import AssessmentItemToolbar from 'frontend/channelEdit/components/AssessmentItemToolbar';
76+
import { AssessmentItemToolbarActions } from 'frontend/channelEdit/constants';
77+
78+
// QTI XML element name → i18n string key, used to build closed-card labels.
79+
const INTERACTION_TYPE_STRING_KEY = {
80+
[QtiInteraction.CHOICE]: 'interactionTypeChoice',
81+
[QtiInteraction.ORDER]: 'interactionTypeOrder',
82+
[QtiInteraction.MATCH]: 'interactionTypeMatch',
83+
[QtiInteraction.TEXT_ENTRY]: 'interactionTypeTextEntry',
84+
[QtiInteraction.EXTENDED_TEXT]: 'interactionTypeExtendedText',
85+
};
86+
87+
export default defineComponent({
88+
name: 'QTIItemEditor',
89+
90+
components: { AssessmentItemToolbar },
91+
92+
setup(props, { emit }) {
93+
const { windowIsSmall } = useKResponsiveWindow();
94+
95+
const questionNumberLabel = computed(() =>
96+
useQTIStr('questionNumberLabel', {
97+
number: props.index,
98+
total: props.total,
99+
}),
100+
);
101+
102+
const questionNumberAndTypeLabel = computed(() => {
103+
const typeKey = INTERACTION_TYPE_STRING_KEY[props.item.type];
104+
const typeLabel = typeKey ? useQTIStr(typeKey) : useQTIStr('interactionTypeUnknown');
105+
return useQTIStr('questionNumberAndTypeLabel', {
106+
number: props.index,
107+
total: props.total,
108+
type: typeLabel,
109+
});
110+
});
111+
const toolbarItemLabel = useQTIStr('toolbarItemLabel');
112+
const closeBtnLabel = useQTIStr('closeBtnLabel');
113+
const questionContentPlaceholder = useQTIStr('questionContentPlaceholder');
114+
115+
const canMoveUp = computed(() => props.index > 1);
116+
const canMoveDown = computed(() => props.index < props.total);
117+
118+
const iconActionsConfig = [
119+
[AssessmentItemToolbarActions.MOVE_ITEM_UP, { collapse: true }],
120+
[AssessmentItemToolbarActions.MOVE_ITEM_DOWN, { collapse: true }],
121+
];
122+
const menuActionsConfig = [
123+
AssessmentItemToolbarActions.ADD_ITEM_ABOVE,
124+
AssessmentItemToolbarActions.ADD_ITEM_BELOW,
125+
AssessmentItemToolbarActions.DELETE_ITEM,
126+
];
127+
128+
function onCardClick(event) {
129+
if (props.isOpen) return;
130+
if (
131+
event.target.closest('.toolbar') !== null ||
132+
event.target.closest('.close-item-btn') !== null
133+
) {
134+
return;
135+
}
136+
emit('open');
137+
}
138+
139+
return {
140+
windowIsSmall,
141+
questionNumberLabel,
142+
questionNumberAndTypeLabel,
143+
toolbarItemLabel,
144+
closeBtnLabel,
145+
questionContentPlaceholder,
146+
canMoveUp,
147+
canMoveDown,
148+
iconActionsConfig,
149+
menuActionsConfig,
150+
onCardClick,
151+
};
152+
},
153+
154+
props: {
155+
/** Assessment item: { id, type (QtiInteraction value), title } */
156+
item: {
157+
type: Object,
158+
required: true,
159+
},
160+
/** 1-based position in the list */
161+
index: {
162+
type: Number,
163+
required: true,
164+
},
165+
/** Total items in the list */
166+
total: {
167+
type: Number,
168+
required: true,
169+
},
170+
/** Whether this card is currently expanded */
171+
isOpen: {
172+
type: Boolean,
173+
default: false,
174+
},
175+
/** Whether to show answers previews for closed items */
176+
displayAnswersPreview: {
177+
type: Boolean,
178+
default: false,
179+
},
180+
},
181+
});
182+
183+
</script>
184+
185+
186+
<style lang="scss" scoped>
187+
188+
.question-card {
189+
--question-card-horizontal-padding: 20px;
190+
191+
position: relative;
192+
min-height: 75px;
193+
padding: 0;
194+
margin-bottom: 16px;
195+
196+
&.closed {
197+
cursor: pointer;
198+
}
199+
}
200+
201+
.question-card-header {
202+
display: flex;
203+
align-items: center;
204+
justify-content: space-between;
205+
padding: 12px var(--question-card-horizontal-padding);
206+
}
207+
208+
.question-card-title {
209+
margin: 0;
210+
font-size: 14px;
211+
font-weight: 600;
212+
}
213+
214+
.question-card-actions {
215+
display: flex;
216+
gap: 8px;
217+
align-items: center;
218+
}
219+
220+
.question-card-body {
221+
min-width: 0;
222+
padding: 10px var(--question-card-horizontal-padding);
223+
}
224+
225+
.question-card-footer {
226+
display: flex;
227+
justify-content: flex-end;
228+
padding: 0 var(--question-card-horizontal-padding) 20px;
229+
}
230+
231+
</style>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export const Cardinality = Object.freeze({
2+
SINGLE: 'single',
3+
MULTIPLE: 'multiple',
4+
ORDERED: 'ordered',
5+
RECORD: 'record',
6+
});
7+
8+
export const BaseType = Object.freeze({
9+
IDENTIFIER: 'identifier',
10+
BOOLEAN: 'boolean',
11+
INTEGER: 'integer',
12+
FLOAT: 'float',
13+
STRING: 'string',
14+
POINT: 'point',
15+
PAIR: 'pair',
16+
DIRECTED_PAIR: 'directedPair',
17+
DURATION: 'duration',
18+
FILE: 'file',
19+
URI: 'uri',
20+
});
21+
22+
export const QtiInteraction = Object.freeze({
23+
CHOICE: 'choiceInteraction',
24+
ORDER: 'orderInteraction',
25+
MATCH: 'matchInteraction',
26+
TEXT_ENTRY: 'textEntryInteraction',
27+
EXTENDED_TEXT: 'extendedTextInteraction',
28+
});

0 commit comments

Comments
 (0)