-
-
Notifications
You must be signed in to change notification settings - Fork 302
feat: Initial Scaffold for QTI editor components and add a development page #5963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| <template> | ||
|
|
||
| <div> | ||
| <div style="padding: 16px 24px 0"> | ||
| <div | ||
| style=" | ||
| padding: 16px; | ||
| color: #2196f3; | ||
| background-color: transparent; | ||
| border: 1px solid #2196f3; | ||
| border-radius: 4px; | ||
| " | ||
| > | ||
| <strong>QTI Editor — Dev Demo</strong> | ||
| Hardcoded items. Changes are local only and not persisted. | ||
| </div> | ||
| </div> | ||
|
|
||
| <QTIEditor | ||
| :assessments="assessments" | ||
| @update="onUpdate" | ||
| /> | ||
| </div> | ||
|
|
||
| </template> | ||
|
|
||
|
|
||
| <script> | ||
|
|
||
| import { ref, defineComponent } from 'vue'; | ||
| import QTIEditor from 'shared/views/QTIEditor/index'; | ||
| import { QtiInteraction } from 'shared/views/QTIEditor/constants'; | ||
|
|
||
| /** | ||
| * Hardcoded items covering three interaction types so the closed-card | ||
| * type label can be visually verified. | ||
| */ | ||
| const INITIAL_ASSESSMENTS = [ | ||
| { | ||
| id: 'demo-item-1', | ||
| type: QtiInteraction.CHOICE, | ||
| title: 'Which planet is closest to the Sun?', | ||
| }, | ||
| { | ||
| id: 'demo-item-2', | ||
| type: QtiInteraction.EXTENDED_TEXT, | ||
| title: 'Describe the water cycle in your own words.', | ||
| }, | ||
| { | ||
| id: 'demo-item-3', | ||
| type: QtiInteraction.ORDER, | ||
| title: 'Arrange these events in chronological order.', | ||
| }, | ||
| ]; | ||
|
|
||
| export default defineComponent({ | ||
| name: 'QTIDemoPage', | ||
|
|
||
| components: { QTIEditor }, | ||
|
|
||
| setup() { | ||
| const assessments = ref(INITIAL_ASSESSMENTS); | ||
|
|
||
| function onUpdate(newList) { | ||
| assessments.value = newList; | ||
| } | ||
|
|
||
| return { assessments, onUpdate }; | ||
| }, | ||
| }); | ||
|
|
||
| </script> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| <template> | ||
|
|
||
| <KPageContainer | ||
| noPadding | ||
| :topMargin="0" | ||
| class="item question-card" | ||
| :class="{ closed: !isOpen }" | ||
| data-test="item" | ||
| @click.native="onCardClick" | ||
| > | ||
| <div | ||
| class="question-card-header" | ||
| :style="{ borderBottom: isOpen ? `1px solid ${$themePalette.grey.v_200}` : 'none' }" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use |
||
| > | ||
| <h3 | ||
| class="question-card-title" | ||
| :style="{ color: $themePalette.grey.v_800 }" | ||
| > | ||
| <template v-if="isOpen"> | ||
| {{ questionNumberLabel }} | ||
| </template> | ||
| <template v-else> | ||
| {{ questionNumberAndTypeLabel }} | ||
| </template> | ||
| </h3> | ||
|
|
||
| <div class="question-card-actions toolbar"> | ||
| <AssessmentItemToolbar | ||
| :iconActionsConfig="iconActionsConfig" | ||
| :menuActionsConfig="menuActionsConfig" | ||
| :displayMenu="true" | ||
| :canMoveUp="canMoveUp" | ||
| :canMoveDown="canMoveDown" | ||
| :canEdit="!isOpen" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The larger consequence: there is no keyboard-accessible way to open a closed card. The only open path is |
||
| :collapse="windowIsSmall" | ||
| :itemLabel="toolbarItemLabel" | ||
| analyticsLabel="QTI Question" | ||
| data-test="toolbar" | ||
| @click="action => $emit('action', action)" | ||
| /> | ||
|
Comment on lines
+28
to
+40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, apologies, I didn't spec this. Could we create a new [{
"icon": "", (string or null)
"label": "", (required)
"handler": () => {},
"collapsed": true/false
}]So... |
||
| </div> | ||
| </div> | ||
|
|
||
| <div | ||
| v-if="isOpen || displayAnswersPreview" | ||
| class="question-card-body" | ||
| > | ||
| <p :style="{ color: $themePalette.grey.v_500, margin: 0, fontStyle: 'italic' }"> | ||
| {{ questionContentPlaceholder }} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div | ||
| v-if="isOpen" | ||
| class="question-card-footer" | ||
| > | ||
| <KButton | ||
| :text="closeBtnLabel" | ||
| class="close-item-btn" | ||
| data-test="closeBtn" | ||
| @click="$emit('close')" | ||
| /> | ||
| </div> | ||
| </KPageContainer> | ||
|
|
||
| </template> | ||
|
|
||
|
|
||
| <script> | ||
|
|
||
| import { computed, defineComponent } from 'vue'; | ||
| import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; | ||
| import { useQTIStr } from '../../qtiEditorStrings'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use the same structure as the communityChannelsStrings file? Where the whole translator is exposed as a named export, and we use destructuring to get the values in the setup. e.g. here. |
||
| import { QtiInteraction } from '../../constants'; | ||
| import AssessmentItemToolbar from 'frontend/channelEdit/components/AssessmentItemToolbar'; | ||
| import { AssessmentItemToolbarActions } from 'frontend/channelEdit/constants'; | ||
|
Comment on lines
+75
to
+76
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, we should always try to avoid importing from other modules outside the QTIEditor as much as possible. |
||
|
|
||
| // QTI XML element name → i18n string key, used to build closed-card labels. | ||
| const INTERACTION_TYPE_STRING_KEY = { | ||
| [QtiInteraction.CHOICE]: 'interactionTypeChoice', | ||
| [QtiInteraction.ORDER]: 'interactionTypeOrder', | ||
| [QtiInteraction.MATCH]: 'interactionTypeMatch', | ||
| [QtiInteraction.TEXT_ENTRY]: 'interactionTypeTextEntry', | ||
| [QtiInteraction.EXTENDED_TEXT]: 'interactionTypeExtendedText', | ||
| }; | ||
|
|
||
| export default defineComponent({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this |
||
| name: 'QTIItemEditor', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: This component emits emits: ['open', 'close', 'action'],
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that'd be a nice addition. |
||
|
|
||
| components: { AssessmentItemToolbar }, | ||
|
|
||
| setup(props, { emit }) { | ||
| const { windowIsSmall } = useKResponsiveWindow(); | ||
|
|
||
| const questionNumberLabel = computed(() => | ||
| useQTIStr('questionNumberLabel', { | ||
| number: props.index, | ||
| total: props.total, | ||
| }), | ||
| ); | ||
|
|
||
| const questionNumberAndTypeLabel = computed(() => { | ||
| const typeKey = INTERACTION_TYPE_STRING_KEY[props.item.type]; | ||
| const typeLabel = typeKey ? useQTIStr(typeKey) : useQTIStr('interactionTypeUnknown'); | ||
| return useQTIStr('questionNumberAndTypeLabel', { | ||
| number: props.index, | ||
| total: props.total, | ||
| type: typeLabel, | ||
| }); | ||
| }); | ||
| const toolbarItemLabel = useQTIStr('toolbarItemLabel'); | ||
| const closeBtnLabel = useQTIStr('closeBtnLabel'); | ||
| const questionContentPlaceholder = useQTIStr('questionContentPlaceholder'); | ||
|
Comment on lines
+111
to
+113
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was there anywhere where we used translators like this? Looks cool, although it's not the new standard we've been using lately. |
||
|
|
||
| const canMoveUp = computed(() => props.index > 1); | ||
| const canMoveDown = computed(() => props.index < props.total); | ||
|
|
||
| const iconActionsConfig = [ | ||
| [AssessmentItemToolbarActions.MOVE_ITEM_UP, { collapse: true }], | ||
| [AssessmentItemToolbarActions.MOVE_ITEM_DOWN, { collapse: true }], | ||
| ]; | ||
| const menuActionsConfig = [ | ||
| AssessmentItemToolbarActions.ADD_ITEM_ABOVE, | ||
| AssessmentItemToolbarActions.ADD_ITEM_BELOW, | ||
| AssessmentItemToolbarActions.DELETE_ITEM, | ||
| ]; | ||
|
|
||
| function onCardClick(event) { | ||
| if (props.isOpen) return; | ||
| if ( | ||
| event.target.closest('.toolbar') !== null || | ||
| event.target.closest('.close-item-btn') !== null | ||
| ) { | ||
| return; | ||
| } | ||
| emit('open'); | ||
| } | ||
|
Comment on lines
+128
to
+137
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, so, this is something we can do differently now. The problem with this is that Keyboard navigation is not possible. So, instead, let's remove the click handler on the entire card, and let's just add an edit button on the toolbar when it is closed. So, let's keep the only way to open the edit mode just being triggered by that edit button. |
||
|
|
||
| return { | ||
| windowIsSmall, | ||
| questionNumberLabel, | ||
| questionNumberAndTypeLabel, | ||
| toolbarItemLabel, | ||
| closeBtnLabel, | ||
| questionContentPlaceholder, | ||
| canMoveUp, | ||
| canMoveDown, | ||
| iconActionsConfig, | ||
| menuActionsConfig, | ||
| onCardClick, | ||
| }; | ||
| }, | ||
|
|
||
| props: { | ||
| /** Assessment item: { id, type (QtiInteraction value), title } */ | ||
| item: { | ||
| type: Object, | ||
| required: true, | ||
| }, | ||
| /** 1-based position in the list */ | ||
| index: { | ||
| type: Number, | ||
| required: true, | ||
| }, | ||
| /** Total items in the list */ | ||
| total: { | ||
| type: Number, | ||
| required: true, | ||
| }, | ||
| /** Whether this card is currently expanded */ | ||
| isOpen: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
|
Comment on lines
+170
to
+174
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of |
||
| /** Whether to show answers previews for closed items */ | ||
| displayAnswersPreview: { | ||
| type: Boolean, | ||
| default: false, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| </script> | ||
|
|
||
|
|
||
| <style lang="scss" scoped> | ||
|
|
||
| .question-card { | ||
| --question-card-horizontal-padding: 20px; | ||
|
|
||
| position: relative; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need it to be position: relative? |
||
| min-height: 75px; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can remove this min-height. |
||
| padding: 0; | ||
| margin-bottom: 16px; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's render a column flex container and add a |
||
|
|
||
| &.closed { | ||
| cursor: pointer; | ||
| } | ||
|
Comment on lines
+196
to
+198
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can remove this now. |
||
| } | ||
|
|
||
| .question-card-header { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| padding: 12px var(--question-card-horizontal-padding); | ||
| } | ||
|
|
||
| .question-card-title { | ||
| margin: 0; | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| } | ||
|
|
||
| .question-card-actions { | ||
| display: flex; | ||
| gap: 8px; | ||
| align-items: center; | ||
| } | ||
|
|
||
| .question-card-body { | ||
| min-width: 0; | ||
| padding: 10px var(--question-card-horizontal-padding); | ||
| } | ||
|
|
||
| .question-card-footer { | ||
| display: flex; | ||
| justify-content: flex-end; | ||
| padding: 0 var(--question-card-horizontal-padding) 20px; | ||
| } | ||
|
|
||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| export const Cardinality = Object.freeze({ | ||
| SINGLE: 'single', | ||
| MULTIPLE: 'multiple', | ||
| ORDERED: 'ordered', | ||
| RECORD: 'record', | ||
| }); | ||
|
|
||
| export const BaseType = Object.freeze({ | ||
| IDENTIFIER: 'identifier', | ||
| BOOLEAN: 'boolean', | ||
| INTEGER: 'integer', | ||
| FLOAT: 'float', | ||
| STRING: 'string', | ||
| POINT: 'point', | ||
| PAIR: 'pair', | ||
| DIRECTED_PAIR: 'directedPair', | ||
| DURATION: 'duration', | ||
| FILE: 'file', | ||
| URI: 'uri', | ||
| }); | ||
|
|
||
| export const QtiInteraction = Object.freeze({ | ||
| CHOICE: 'choiceInteraction', | ||
| ORDER: 'orderInteraction', | ||
| MATCH: 'matchInteraction', | ||
| TEXT_ENTRY: 'textEntryInteraction', | ||
| EXTENDED_TEXT: 'extendedTextInteraction', | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, let's just use a hardcoded string 😅, so that we don't forget to remove this constant later.